Context
Every middleware receives a ctx object as its only argument. It contains everything about the incoming request, the server configuration, and some useful helpers.
export default server()
.get('/hello', (ctx) => {
console.log(ctx.method); // 'get'
console.log(ctx.url.pathname); // '/hello'
return 'Hello world';
});The ctx object carries the following:
ctx.method: the HTTP method, lowercased ('get','post', ...).ctx.url: the request URL, extended withparamsandquery.ctx.ip: the client IP, proxy-aware throughtrustProxy.ctx.body: the parsed body, aBufferor a stream, per thebodyoption.ctx.headers: the request headers as a plain object, lowercased keys.ctx.cookies: the parsed request cookies as a plain object.ctx.session: the current session data, persisted in the session store.ctx.user: the authenticated user, whenauthis configured.ctx.options: the resolved server settings.ctx.time: aServer-Timinghelper to measure segments of a request.ctx.platform: info about the runtime (runtime,production,provider).ctx.socket: the active WebSocket connection, inside.socket()handlers.ctx.sockets: every open WebSocket connection, for broadcasting.
ctx.method
The HTTP method of the request, lowercased:
.get('/example', (ctx) => {
console.log(ctx.method); // 'get'
});Possible values: 'get', 'post', 'put', 'patch', 'delete', 'head', 'options'.
ctx.url
The full URL of the request, extended with params and query:
.get('/users/:id', (ctx) => {
console.log(ctx.url.pathname); // '/users/42'
console.log(ctx.url.params.id); // '42'
console.log(ctx.url.query.page); // '2' (from ?page=2)
console.log(ctx.url.hostname); // 'localhost'
});ctx.url.params: URL path parameters, e.g./users/:id→ctx.url.params.id. Types can be inferred; see Parameters.ctx.url.query: Parsed query string as a plain object, e.g.?page=2&sort=asc→{ page: '2', sort: 'asc' }.- All standard
URLproperties are available (pathname,hostname,href,origin, etc.). - Use
ctx.url.protocolto check the scheme ('https:'for a secure request).
ctx.ip
The IP address of the client making the request:
.get('/', (ctx) => {
console.log(ctx.ip); // '203.0.113.5'
});Behind a proxy, the real client IP is read from forwarding headers, governed by the security.trustProxy option (on by default). Set security: { trustProxy: false } when clients connect directly.
ctx.body
The request body. By default it is parsed by content-type; the body option controls how it is read.
.post('/users', (ctx) => {
console.log(ctx.body.name); // 'Francisco'
console.log(ctx.body.email); // '[email protected]'
});With the default parse mode:
- For
application/jsonrequests, it is the parsed JSON value. - For
application/x-www-form-urlencodedormultipart/form-datatext fields, it is a plain object of field values. A repeated field name collects into an array (HTML'sname="tags[]"works too; the[]is stripped). - For file uploads, the field value is an
UploadedFileobject{ name, id, path, type, size }(or an array of them for a repeated field name). Files stream touploadsas they arrive, so their bytes are never all in memory. See File handling. - For a raw non-form body with a binary content-type (e.g.
image/png,video/mp4), the whole body is streamed touploadsas oneUploadedFile. With nouploadsconfigured it is the rawBufferinstead. undefinedfor methods that do not send a body (GET,DELETE, etc.).
The other body modes give ctx.body a different shape:
body | ctx.body |
|---|---|
parse (default) | the parsed object / file references above |
raw | a Buffer of the unparsed bytes |
stream | the request's web ReadableStream, unread |
ctx.headers
The request headers as a plain object, with lowercased keys:
.get('/', (ctx) => {
// 'application/json'
console.log(ctx.headers['content-type']);
// 'Bearer ...'
console.log(ctx.headers['authorization']);
});To set headers in the response, use the headers() reply helper.
ctx.cookies
The parsed request cookies as a plain object:
.get('/', (ctx) => {
console.log(ctx.cookies.token); // 'abc123'
});To set cookies in the response, use the cookies() reply helper.
ctx.session
The session data for the current user. The session is persisted in the store configured via the session option. An empty object {} when no session exists.
export default server({ session: store })
.post('/login', (ctx) => {
ctx.session.userId = 42;
return 200;
})
.get('/me', (ctx) => {
return { id: ctx.session.userId };
});You can type the session by passing a generic to server():
type MySession = { userId: number };
export default server<MySession>()
.get('/me', (ctx) => {
// ctx.session.userId is typed as number
return { id: ctx.session.userId };
});ctx.user
Requires the
authoption to be configured.
The authenticated user, populated when a user is logged in. undefined when not authenticated. Available on HTTP routes and, resolved from the handshake, inside .socket() handlers too.
.get('/profile', (ctx) => {
if (!ctx.user) return 401;
return { name: ctx.user.name, email: ctx.user.email };
});Standard fields always present on ctx.user:
id: unique user identifieremail: user emailprovider: the auth provider used ('github','email', etc.)strategy: the auth strategy ('cookie','jwt','token','key')
You can type the user by passing a second generic to server():
type MyUser = { id: number; name: string; email: string };
export default server<{}, MyUser>()
.get('/profile', (ctx) => {
return ctx.user?.name;
});ctx.options
The resolved server settings (the processed version of the options passed to server()):
.get('/', (ctx) => {
console.log(ctx.options.port); // 3000
});Mainly useful in middleware that needs to inspect server configuration.
ctx.time
A timing helper for Server-Timing. Call ctx.time('label') to mark a point; the time spent in each labeled segment (since the previous mark) is sent back in the Server-Timing response header, which browsers show in their Network panel. No header is added if you never call it.
.get('/report', async (ctx) => {
const rows = await db.query('...');
// time from start of request to here
ctx.time('query');
const html = render(rows);
// time since the previous mark
ctx.time('render');
return html;
});
// Server-Timing: query;dur=42, render;dur=8ctx.platform
Information about the runtime environment:
.get('/', (ctx) => {
// 'node' | 'bun' | 'cloudflare' | ...
console.log(ctx.platform.runtime);
// true | false
console.log(ctx.platform.production);
// 'netlify' | 'aws' | null | ...
console.log(ctx.platform.provider);
});ctx.socket
The WebSocket connection for the current event, available inside .socket() handlers (open, message, close). It's the single client this event is about; call ctx.socket.send(data) to reply to just them.
export default server()
.get('/', () => file('./index.html'))
.socket('open', (ctx) => {
ctx.socket.send('Welcome!');
})
.socket('message', (ctx) => {
// ctx.body is what this client sent
ctx.socket.send(`You said: ${ctx.body}`);
});send() accepts a string or binary data. On a message event the incoming payload is on ctx.body. WebSockets work on both Node and Bun; see the WebSockets guide for the full picture.
ctx.sockets
Every open WebSocket connection, as an array, available inside .socket() handlers. A connection is added when it opens and removed when it closes, so iterating ctx.sockets is how you broadcast to everyone.
.socket('message', (ctx) => {
// Relay this message to every connected client
for (const socket of ctx.sockets) {
socket.send(ctx.body);
}
});To broadcast to everyone except the sender, skip ctx.socket:
.socket('message', (ctx) => {
for (const socket of ctx.sockets) {
if (socket === ctx.socket) continue;
socket.send(ctx.body);
}
});