Context

The context object c is passed to every route handler. It provides access to the request, locals set by onRequest, and validated input when a request schema is defined.

route.get("/books/:id", {
  request: { params: z.object({ id: z.uuid() }) },
  resolve: (c) => {
    c.request; // Web Standard Request
    c.input; // Validated input (body, query, params) — only when request schemas are defined
    c.locals; // Data from onRequest
  },
});

c.request

Type: Request

The original Web Standard Request object. Use it for anything not covered by c.input: headers, raw body, URL, method.

resolve: (c) => {
  const auth = c.request.headers.get("authorization");
  const url = new URL(c.request.url);
  const method = c.request.method;
  const body = await c.request.text(); // raw body
},

c.input

Type: InputState

Contains the result of Zod validation for body, query, and params. Only available on routes that define at least one request schema (params, query, or body).

c.input.ok

Type: boolean

true if all schemas passed validation. false if any schema failed.

resolve: (c) => {
  if (!c.input.ok) {
    return Response.json({ error: c.input.issues }, { status: 400 });
  }
  // Safe to use c.input.body, c.input.query, c.input.params
},

c.input.body

Type: Inferred from the body Zod schema, or undefined

The validated request body. Only available when c.input.ok is true and a body schema is defined.

route.post("/books", {
  request: { body: z.object({ title: z.string(), genre: z.string() }) },
  resolve: (c) => {
    if (!c.input.ok) return Response.json({ error: c.input.issues }, { status: 400 });
    const { title, genre } = c.input.body; // typed: { title: string, genre: string }
  },
});

c.input.query

Type: Inferred from the query Zod schema, or undefined

The validated query parameters. Query strings are parsed from the URL automatically.

route.get("/books", {
  request: {
    query: z.object({ page: z.coerce.number().default(1), genre: z.string().optional() }),
  },
  resolve: (c) => {
    if (!c.input.ok) return Response.json({ error: c.input.issues }, { status: 400 });
    const { page, genre } = c.input.query; // typed: { page: number, genre?: string }
  },
});

c.input.params

Type: Inferred from the params Zod schema

Path parameters extracted from the URL pattern, validated against the schema.

route.get("/books/:id", {
  request: { params: z.object({ id: z.uuid() }) },
  resolve: (c) => {
    if (!c.input.ok) return Response.json({ error: "Invalid ID" }, { status: 400 });
    const { id } = c.input.params; // validated UUID string
  },
});

c.input.issues

Type: Zod issue array, or undefined

Validation error details when c.input.ok is false. Contains field-level error messages from Zod.

if (!c.input.ok) {
  return Response.json(
    {
      error: {
        code: "VALIDATION_ERROR",
        message: "Invalid input",
        issues: c.input.issues,
      },
    },
    { status: 400 },
  );
}

c.locals

Type: The return type of onRequest

Data set by the onRequest callback. Use it for request IDs, authenticated user info, timing, or any per-request state.

// In setup:
onRequest: ({ request }) => {
  return { requestId: crypto.randomUUID(), userId: "user-1", startTime: Date.now() };
},

// In onResponse (locals are typed here):
onResponse: ({ response, locals }) => {
  response.headers.set("X-Request-Id", locals.requestId);
  return response;
},

If onRequest is not defined or returns void, c.locals is an empty object.

Locals are fully typed inside lifecycle hooks (onResponse, onError, onNotFound) because the framework infers the return type of onRequest. Inside route handlers, c.locals is available but not typed. If you need to access locals in a handler, you must provide your own types:

resolve: (c) => {
  const locals = c.locals as { requestId: string; startTime: number };
  console.log(locals.requestId);
},

For authentication, use a helper function that reads from c.request directly instead of relying on locals. See the guide for the recommended pattern.

Common patterns

Accessing raw body (for webhooks)

resolve: async (c) => {
  const rawBody = await c.request.text();
  const signature = c.request.headers.get("x-signature");
  // Verify signature against rawBody
},

Getting the URL path

resolve: (c) => {
  const url = new URL(c.request.url);
  const path = url.pathname;     // "/v2/books"
  const search = url.search;     // "?genre=fiction"
},