Handlers
A handler receives one context object and returns the response body.
.post("/users", ({ body, params, query, request, set, db }) => {
// ...
})The context
| Field | Type | Available |
|---|---|---|
request | the standard Request | always |
set | { status: number; headers: Headers } | always |
signal | an AbortSignal, aborted on timeout or when the client disconnects | always |
body | inferred from the body schema | when a body schema is given |
params | inferred from the params schema | when a params schema is given |
query | inferred from the query schema | when a query schema is given |
db | your typed Drizzle instance | when a database is configured |
body, params and query don't exist without a schema: TypeScript won't let you use them. To read raw data instead, use request:
.get("/raw", async ({ request }) => {
const url = new URL(request.url);
return { search: url.searchParams.get("q") };
})Handlers can be async:
.get("/users", async ({ db }) => {
const users = await db.query.users.findMany();
return { users };
})Returning a response
What the handler returns becomes the response body:
| You return | Body | Content-Type |
|---|---|---|
a string | the string | text/plain; charset=utf-8 |
an object, an array or null | JSON | application/json |
| nothing | empty | none |
.get("/text", () => "Hello") // text/plain
.get("/json", () => ({ hello: "world" })) // application/json
.post("/jobs", ({ set }) => { // empty body, 202
set.status = 202;
})WARNING
Returning a Response object is not supported: it would be sent as JSON.
What you return is sent as is. Return a DTO built for the client, not a database row: see Never return a database row.
Status and headers
Change the status or add headers with set:
.post("/users", ({ body, set }) => {
set.status = 202;
set.headers.set("Location", `/users/${body.id}`);
set.headers.set("Cache-Control", "no-store");
return { queued: true };
})set.statusstarts at the method's default:200for GET and PUT,201for POST,204for DELETE.set.headersis a standardHeadersobject. If you setContent-Type, Lyn keeps it:
.get("/", ({ set }) => {
set.headers.set("Content-Type", "text/html; charset=utf-8");
return "<h1>Hello</h1>";
})DELETE returns no body by default
204 No Content has no body, so what a .delete() handler returns is dropped. Set set.status = 200 to send it.
Every response also gets an X-Request-Id header, which matches the request's log line, and X-Content-Type-Options: nosniff, so browsers never run a JSON or text response as another type. Set your own X-Content-Type-Options with set.headers to replace it.
Timeouts and cancellation
A request has 30 seconds, body included. After that Lyn answers 408 REQUEST_TIMEOUT. Change it with server.requestTimeoutMs.
Lyn can't stop your code, but it tells it to stop: signal is aborted at the timeout, or as soon as the client disconnects. Pass it to fetch, or check signal.aborted between long steps:
.get("/weather", async ({ signal }) => {
const response = await fetch("https://api.example.com/weather", { signal });
return response.json();
})With a database, Postgres also cancels any query that runs longer than the request timeout.
Not supported yet
- Streaming, files or a custom
Response.