Routing
Routing is how Fast decides which piece of your code runs for a given URL. Under the hood it is a regex-free segment trie, which is a fancy way of saying it matches paths with fast hash lookups instead of running a regular expression for every route. You do not have to think about that most of the time, but it is why routing stays quick even with a lot of routes.
Registering routes
The verb helpers cover the usual HTTP methods. Each takes a path and a handler:
get('/', fn (): string => 'Home');
post('/users', fn () => json(['created' => true]));
put('/users/{id}', UpdateUser::class);
patch('/users/{id}', 'UpdateUser@handle');
delete('/users/{id}', fn () => text('gone'));
any('/health', fn (): string => 'ok');
get() also answers HEAD requests automatically, so you do not need to register those separately. If you want to handle a specific set of methods, reach for route():
route(['PUT', 'PATCH'], '/users/{id}', UpdateUser::class);
Route parameters
Two dynamic forms are supported. The first captures exactly one path segment:
get('/users/{id}', fn () => json(['id' => request()->attribute('id')]));
The second is a trailing catch-all that grabs everything remaining. It must be the last segment, and it matches zero or more segments:
get('/files/{path*}', fn () => text(request()->attribute('path')));
That route matches /files/a/b/c with path set to "a/b/c", and it also matches a bare /files with path set to an empty string. You read both kinds of parameter the same way, through request()->attribute('name').
How matching decides
When more than one route could match, Fast follows a clear precedence order: static beats dynamic beats catch-all, and it backtracks when needed. So a fixed segment and a {param} can happily sit at the same position, and the exact match always wins regardless of the order you registered them in.
get('/users/me', fn (): string => 'current user'); // static, wins for /users/me
get('/users/{id}', fn () => 'some user'); // dynamic, handles the rest
If both an exact endpoint and a catch-all are registered at the same spot, the exact endpoint wins for any method it handles. The catch-all only fills in for methods the exact route does not cover.
Per-parameter regex constraints like
{id:\d+}are intentionally not supported. A constrained segment throws at registration time. Validate the value inside your handler instead, which keeps routing simple and fast.
Handlers come in many shapes
A handler can be almost anything callable:
- a closure or plain function;
- an invokable class, like
CreateUser::class; - an array callable, like
[UserController::class, 'store']; - a string in
"Class@method"form.
Class handlers are resolved through the container, so their constructor dependencies get autowired. Whatever form you use, the return value follows the same rule everywhere in Fast: a Response is sent as-is, a string becomes HTML, and anything else becomes JSON.
Once you can route requests, the next tool to reach for is Middleware.