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.