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 with params and query.
  • ctx.ip: the client IP, proxy-aware through trustProxy.
  • ctx.body: the parsed body, a Buffer or a stream, per the body option.
  • 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, when auth is configured.
  • ctx.options: the resolved server settings.
  • ctx.time: a Server-Timing helper 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/:idctx.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 URL properties are available (pathname, hostname, href, origin, etc.).
  • Use ctx.url.protocol to 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/json requests, it is the parsed JSON value.
  • For application/x-www-form-urlencoded or multipart/form-data text fields, it is a plain object of field values. A repeated field name collects into an array (HTML's name="tags[]" works too; the [] is stripped).
  • For file uploads, the field value is an UploadedFile object { name, id, path, type, size } (or an array of them for a repeated field name). Files stream to uploads as 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 to uploads as one UploadedFile. With no uploads configured it is the raw Buffer instead.
  • undefined for methods that do not send a body (GET, DELETE, etc.).

The other body modes give ctx.body a different shape:

bodyctx.body
parse (default)the parsed object / file references above
rawa Buffer of the unparsed bytes
streamthe 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 auth option 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 identifier
  • email: user email
  • provider: 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=8

ctx.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);
  }
});