Routes

Routes map HTTP methods and URL patterns to handler functions.

import { route } from "@hectoday/http";

Methods

route.get(path, config);
route.post(path, config);
route.put(path, config);
route.patch(path, config);
route.delete(path, config);

All methods have the same signature: (path: string, config: RouteConfig) => RouteDescriptor.

Route config

route.get("/books", {
  request: { query: BookQuerySchema }, // Optional: Zod validation
  response: { 200: BookListSchema }, // Optional: for OpenAPI
  resolve: (c) => Response.json(books), // Required: handler
});

resolve

Type: (c: Context) => Response | Promise<Response>Required

The route handler. Receives a context object and must return a Response (or a Promise<Response> for async handlers).

// Sync
resolve: (c) => Response.json({ ok: true }),

// Async
resolve: async (c) => {
  const data = await fetchFromDatabase();
  return Response.json(data);
},

request

Type: { body?: ZodSchema; query?: ZodSchema; params?: ZodSchema } — Optional

Zod schemas for request validation. See Validation.

request: {
  body: z.object({ title: z.string(), genre: z.enum(["fiction", "non-fiction"]) }),
  query: z.object({ page: z.coerce.number().default(1) }),
},

response

Type: Record<number, ZodSchema> — Optional

Zod schemas for response documentation, keyed by HTTP status code. Used by @hectoday/openapi to generate the OpenAPI spec. Does not perform runtime validation on outgoing responses.

response: {
  200: BookSchema,
  404: ErrorSchema,
},

Path parameters

Use :param syntax for path parameters. To access them, you need a request.params schema. This validates the parameters and makes them available on c.input.params with full type inference:

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

Multiple parameters work the same way:

route.get("/authors/:authorId/books/:bookId", {
  request: {
    params: z.object({ authorId: z.uuid(), bookId: z.uuid() }),
  },
  resolve: (c) => {
    if (!c.input.ok) {
      return Response.json({ error: "Invalid IDs" }, { status: 400 });
    }
    const { authorId, bookId } = c.input.params;
  },
});

c.input is only available on routes that define at least one request schema (params, query, or body). Without a schema, use c.request directly to access the raw request.

Route registration order

Routes are matched in the order they are registered. Put specific routes before general ones:

routes: [
  route.get("/books/top", { /* ... */ }),     // Matches first
  route.get("/books/:id", { /* ... */ }),      // Matches if /books/top didn't
],

If /books/:id is registered first, a request to /books/top would match it with id = "top".

Returning responses

Route handlers return Web Standard Response objects:

// JSON
Response.json({ title: "Kindred" });
Response.json({ title: "Kindred" }, { status: 201 });

// Plain text
new Response("OK", { status: 200 });

// Empty (204 No Content)
new Response(null, { status: 204 });

// With headers
new Response(JSON.stringify(data), {
  status: 200,
  headers: {
    "Content-Type": "application/json",
    "Cache-Control": "public, max-age=60",
  },
});

// Redirect
Response.redirect("https://example.com/new-location", 301);