Skip to content

Routing ​

Declaring routes ​

An app is a chain of routes. Each method returns the app, so you can keep chaining:

ts
import { Lyn, z } from "@rouret/lyn";

const users = new Map<string, { name: string }>();
const idSchema = z.object({ id: z.string() });
const userSchema = z.object({ name: z.string().min(1) });

const app = new Lyn()
  .get("/users", () => [...users.values()])
  .post("/users", ({ body }) => {
    const id = crypto.randomUUID();
    users.set(id, body);
    return { id, ...body };
  }, { body: userSchema })
  .put("/users/:id", ({ params, body }) => {
    users.set(params.id, body);
    return { id: params.id, ...body };
  }, { params: idSchema, body: userSchema })
  .delete("/users/:id", ({ params }) => {
    users.delete(params.id);
  }, { params: idSchema });

await app.listen(3000);

Every method takes the same three arguments:

ts
app.get(path, handler, validation?)
  • path: where the route lives (see Paths).
  • handler: the function that answers (see Handlers).
  • validation: optional Zod schemas for body, params and query (see Validation).
MethodDefault statusAccepts a body schema
.get()200no
.post()201yes
.put()200yes
.delete()204yes

OPTIONS requests are answered for you.

Paths ​

A path starts with / and is made of segments of three kinds:

SegmentExampleMatches
Static/usersexactly /users
Parameter/users/:id/users/42, /users/ada → params.id
Wildcard/files/*/files and everything below it
  • Parameter values are URL-decoded: /users/j%C3%A9r%C3%B4me gives "jérôme".
  • A wildcard must be the last segment.
  • Trailing and double slashes are ignored: /users/ matches /users.

To read a parameter in your handler, declare it in a params schema.

Which route wins ​

When several routes match, the most specific wins, segment by segment: static, then parameter, then wildcard. These three routes can coexist:

ts
new Lyn()
  .get("/users/me", () => "me")          // GET /users/me
  .get("/users/:id", () => "one user")   // GET /users/42
  .get("/users/*", () => "fallback");    // GET /users/42/avatar

Declaration order doesn't matter: /users/me wins even when it is declared last.

When nothing matches ​

RequestResponse
No route has this path404 NOT_FOUND
The path exists, but not for this method405 METHOD_NOT_ALLOWED, with an Allow header
OPTIONS on an existing path204, with an Allow header (and CORS headers when enabled)

Mistakes caught at startup ​

A badly declared route throws as soon as it is registered, before the server starts:

MistakeError
Empty pathRoute path cannot be empty
No leading slashRoute path must start with "/": users
Wildcard not lastWildcard must be the last segment: /files/*/meta
Same method and path twiceRoute GET /users/:userId is already registered
Params schema key missing from the pathParams schema of GET /users/:id declares "userId", which is not a param of the path

/users/:id and /users/:userId count as the same path.

Starting the server ​

Routes answer once the app is started with listen(), or through handle() in tests. See Server.

Not supported yet

  • PATCH, HEAD and other methods.
  • Route groups, prefixes, middlewares and plugins.

Released under the MIT License.