setup()

Creates an application with lifecycle callbacks and routes.

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

const app = setup({
  onRequest: ({ request }) => {
    /* ... */
  },
  onResponse: ({ request, response, locals }) => {
    /* ... */
    return response;
  },
  onError: ({ error, request, locals }) => {
    /* ... */
  },
  onNotFound: ({ request, locals }) => {
    /* ... */
  },
  routes: [
    /* ... */
  ],
});

Return value

setup() returns an object with fetch, request, and routes:

const app = setup({
  routes: [
    /* ... */
  ],
});

// Use with srvx
serve({ fetch: app.fetch, port: 3000 });

// Use in tests
const res = await app.request("/health");

app.fetch has the signature (request: Request) => Response | Promise<Response>. It works with any server that accepts this signature: srvx, Bun, Deno, Cloudflare Workers.

app.request is a convenience method for testing. It takes a path and optional options, builds a Request internally, and returns a Promise<Response>.

interface RequestOptions {
  method?: string;
  body?: unknown;
  headers?: Record<string, string>;
  query?: Record<string, string | string[]>;
}

app.routes is the array of route descriptors, useful for passing to openapi().

Configuration options

routes

Type: RouteDescriptor[]Required

Array of route definitions created with route.get(), route.post(), etc.

setup({
  routes: [
    route.get("/books", { resolve: () => Response.json([]) }),
    route.post("/books", {
      resolve: (c) => {
        /* ... */
      },
    }),
  ],
});

onRequest

Type: (args: { request: Request }) => TLocals | Promise<TLocals> | void | Promise<void>

Runs before every route handler. Use it for request IDs, timing, or any per-request setup.

Returns an object that becomes c.locals in route handlers. If it returns void or undefined, locals is an empty object.

onRequest: ({ request }) => {
  return { requestId: crypto.randomUUID(), startTime: Date.now() };
},

To reject a request early, throw a Response. Thrown Response objects bypass onError and onResponse and are returned directly to the client.

onRequest: ({ request }) => {
  const key = request.headers.get("x-api-key");
  if (key !== "valid-key") {
    throw Response.json(
      { error: "Invalid API key" },
      { status: 401 },
    );
  }
  return {};
},

onResponse

Type: (args: { request: Request; response: Response; locals: TLocals }) => Response | Promise<Response>

Runs after every route handler. Use it for logging, adding headers, or collecting metrics. Must return a Response.

onResponse: ({ request, response, locals }) => {
  const duration = Date.now() - locals.startTime;
  response.headers.set("X-Request-Id", locals.requestId);
  response.headers.set("X-Response-Time", `${duration}ms`);
  return response;
},

You can modify the response headers and return the same response, or return an entirely new Response.

onError

Type: (args: { error: Error; request: Request; locals: Partial<TLocals> }) => Response | Promise<Response>

Runs when a route handler or onRequest throws an Error. Must return a Response.

locals is Partial<TLocals> because onRequest may not have completed before the error occurred.

onError: ({ error, request, locals }) => {
  console.error("Unhandled error:", error.message);

  return Response.json(
    { error: { code: "INTERNAL_ERROR", message: "An unexpected error occurred" } },
    { status: 500 },
  );
},

Thrown Response objects bypass onError. Only thrown Error objects reach onError.

onNotFound

Type: (args: { request: Request; locals: TLocals }) => Response | Promise<Response>

Runs when no route matches the request. Returns a custom 404 response.

onNotFound: ({ request, locals }) => {
  const url = new URL(request.url);
  return Response.json(
    { error: { code: "NOT_FOUND", message: `No route for ${request.method} ${url.pathname}` } },
    { status: 404 },
  );
},

If not provided, the framework returns a default 404 response.

Lifecycle order

Request arrives
    |
    v
onRequest({ request })
    | returns locals (or throws)
    v
Route handler ({ request, input, locals })
    | returns Response (or throws)
    v
onResponse({ request, response, locals })
    | returns Response
    v
Response sent to client

If onRequest or the route handler throws an Error:

throw Error
    |
    v
onError({ error, request, locals })
    | returns Response
    v
onResponse({ request, response, locals })
    |
    v
Response sent to client

If onRequest or the route handler throws a Response:

throw Response
    |
    v
Response sent to client (bypasses onError AND onResponse)