Reference

This is the complete API reference. Use it when you need exact details about types, signatures, and behavior.

For concepts and examples, see the earlier chapters. This is just the facts.

Core types

Context

The context object passed to guards and handlers.

interface Context<
  TParams = unknown,
  TQuery = unknown,
  TBody = unknown
> {
  request: Request;
  raw: RawValues;
  input: InputState<TParams, TQuery, TBody>;
  locals: Record<string, unknown>;
}

Properties:

Type parameters:

RawValues

Extracted inputs from the request, not validated.

interface RawValues {
  params: Record<string, string | undefined>;
  query: Record<string, string | string[] | undefined>;
  body?: unknown;
}

Properties:

Notes:

InputState

Result of validation. Either all inputs are valid, or some failed.

type InputState<TParams, TQuery, TBody> =
  | InputOk<TParams, TQuery, TBody>
  | InputErr;

InputOk

When validation passes:

interface InputOk<TParams, TQuery, TBody> {
  ok: true;
  params: TParams;
  query: TQuery;
  body: TBody;
}

Properties:

Type safety: TypeScript infers exact types from your schemas.

InputErr

When validation fails:

interface InputErr {
  ok: false;
  failed: ValidationPart[];
  issues: ValidationIssue[];
  received: {
    params?: unknown;
    query?: unknown;
    body?: unknown;
  };
  errors?: Partial<Record<ValidationPart, unknown>>;
}

Properties:

ValidationIssue

Normalized validation error:

interface ValidationIssue {
  part: ValidationPart;
  path: readonly string[];
  message: string;
  code?: string;
}

Properties:

ValidationPart

type ValidationPart = "params" | "query" | "body";

Which part of the request is being validated.

Handler

A route descriptor returned by route.*() functions:

interface Handler {
  method: string | string[];
  path: string;
  handler: HandlerFn;
  guards?: GuardFn[];
  request?: RequestSchemas<SchemaLike>;
}

Properties:

Notes:

HandlerFn

The function that handles the request:

type HandlerFn<TParams = unknown, TQuery = unknown, TBody = unknown> = (
  c: Context<TParams, TQuery, TBody>
) => Response | Promise<Response>;

Parameters:

Returns:

Notes:

RouteParams

Type helper to extract param types from a path pattern:

type RouteParams<T extends string> = /* implementation */

Usage:

type Params = RouteParams<"/users/:id">; // { id: string }
type Params2 = RouteParams<"/orgs/:orgId/repos/:repoId">; 
// { orgId: string; repoId: string }

Notes:

Route functions

route.get()

function get<TPath extends string>(
  path: TPath,
  config: RouteConfig
): Handler

Create a GET route.

Parameters:

Returns: Handler descriptor

Example:

route.get("/users/:id", {
  resolve: (c) => Response.json({ id: c.raw.params.id })
})

route.post()

function post<TPath extends string>(
  path: TPath,
  config: RouteConfig
): Handler

Create a POST route.

route.put()

function put<TPath extends string>(
  path: TPath,
  config: RouteConfig
): Handler

Create a PUT route.

route.patch()

function patch<TPath extends string>(
  path: TPath,
  config: RouteConfig
): Handler

Create a PATCH route.

route.delete()

function delete<TPath extends string>(
  path: TPath,
  config: RouteConfig
): Handler

Create a DELETE route.

route.head()

function head<TPath extends string>(
  path: TPath,
  config: RouteConfig
): Handler

Create a HEAD route.

route.options()

function options<TPath extends string>(
  path: TPath,
  config: RouteConfig
): Handler

Create an OPTIONS route.

route.all()

function all<TPath extends string>(
  path: TPath,
  config: RouteConfig
): Handler

Create a route that matches all HTTP methods.

route.on()

function on<TPath extends string>(
  method: string,
  path: TPath,
  config: RouteConfig
): Handler

Create a route for a custom HTTP method.

Parameters:

Example:

route.on("PROPFIND", "/webdav", {
  resolve: () => new Response("WebDAV response")
})

RouteConfig

interface RouteConfig<
  TParamsSchema = unknown,
  TQuerySchema = unknown,
  TBodySchema = unknown
> {
  request?: RequestSchemas<TParamsSchema, TQuerySchema, TBodySchema>;
  guards?: GuardFn[];
  resolve: HandlerFn;
}

Properties:

RequestSchemas

interface RequestSchemas<
  TParamsSchema = unknown,
  TQuerySchema = unknown,
  TBodySchema = unknown
> {
  params?: TParamsSchema;
  query?: TQuerySchema;
  body?: TBodySchema;
}

Properties:

Example:

request: {
  params: z.object({ id: z.string().uuid() }),
  query: z.object({ include: z.string().optional() }),
  body: z.object({ name: z.string() })
}

Setup and Configuration

setup()

function setup(config: Config | Handler[]): {
  fetch: (req: Request) => Promise<Response>;
}

Bootstrap the Hectoday HTTP application.

Parameters:

Returns: Object with fetch method

Example:

const app = setup({
  handlers: [...],
  validator: zodValidator,
  onRequest: ({ request }) => ({ requestId: crypto.randomUUID() }),
  onResponse: ({ context, response }) => response,
  onError: ({ error, context }) => Response.json({ error: "Internal error" }, { status: 500 })
});

Config

interface Config {
  handlers: Handler[];
  validator?: Validator<SchemaLike>;
  onRequest?: OnRequestHandler;
  onResponse?: OnResponseHandler;
  onError?: OnErrorHandler;
}

Properties:

OnRequestHandler

type OnRequestHandler = (
  info: { request: Request }
) => void | Record<string, unknown> | Promise<void | Record<string, unknown>>;

Runs before routing, receives the raw Request.

Parameters:

Returns:

Parameter Styles:

You can use either style:

// Destructured (concise)
onRequest: ({ request }) => {
  return { requestId: crypto.randomUUID() };
}

// Named parameter (explicit)
onRequest: (info) => {
  const { request } = info;
  return { requestId: crypto.randomUUID() };
}

Notes:

Example:

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

OnResponseHandler

type OnResponseHandler = (
  info: { context: Context; response: Response }
) => Response | Promise<Response>;

Runs after handler, can modify the response.

Parameters:

Returns:

Parameter Styles:

You can use either style:

// Destructured (concise) - use only what you need
onResponse: ({ response }) => {
  const headers = new Headers(response.headers);
  headers.set("x-powered-by", "hectoday");
  return new Response(response.body, { status: response.status, headers });
}

// Named parameter (explicit)
onResponse: (info) => {
  const { context, response } = info;
  const headers = new Headers(response.headers);
  headers.set("x-request-id", context.locals.requestId);
  return new Response(response.body, { status: response.status, headers });
}

Example:

onResponse: ({ context, response }) => {
  const headers = new Headers(response.headers);
  headers.set("X-Request-Id", String(context.locals.requestId));
  
  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers
  });
}

OnErrorHandler

type OnErrorHandler = (
  info: { error: unknown; context: Context }
) => Response | Promise<Response>;

Handles unexpected errors that escape handlers.

Parameters:

Returns:

Parameter Styles:

You can use either style:

// Destructured (concise)
onError: ({ error, context }) => {
  console.error("Error:", error);
  return Response.json({ error: "Internal Error" }, { status: 500 });
}

// Named parameter (explicit)
onError: (info) => {
  const { error, context } = info;
  console.error(`Error for ${context.request.url}:`, error);
  return Response.json({ error: "Internal Error" }, { status: 500 });
}

// Only need one property? Just destructure that
onError: ({ error }) => {
  console.error(error);
  return Response.json({ error: "Internal Error" }, { status: 500 });
}

Notes:

Example:

onError: ({ error, context }) => {
  console.error("Unexpected error:", {
    error,
    requestId: context.locals.requestId,
    path: context.request.url
  });
  
  return Response.json(
    { error: "Internal server error" },
    { status: 500 }
  );
}

Guard API

GuardFn

type GuardFn = (c: Context) => GuardResult | Promise<GuardResult>;

A function that makes an allow/deny decision.

Parameters:

Returns:

Notes:

GuardResult

type GuardResult =
  | { allow: true; locals?: Record<string, unknown> }
  | { deny: Response };

The result of a guard.

Allow result

{ allow: true; locals?: Record<string, unknown> }

Properties:

Example:

return { allow: true, locals: { userId: "123" } };

Deny result

{ deny: Response }

Properties:

Example:

return { deny: Response.json({ error: "Forbidden" }, { status: 403 }) };

Guard example

const requireAuth: GuardFn = (c) => {
  const token = c.request.headers.get("authorization");
  
  if (!token) {
    return {
      deny: Response.json({ error: "Unauthorized" }, { status: 401 })
    };
  }
  
  const user = verifyToken(token);
  
  if (!user) {
    return {
      deny: Response.json({ error: "Invalid token" }, { status: 401 })
    };
  }
  
  return { allow: true, locals: { user, userId: user.id } };
};

Group API

group()

function group(options: GroupOptions): Handler[]

Apply guards to multiple handlers.

Parameters:

Returns: Array of handlers with guards prepended

Example:

const adminRoutes = group({
  guards: [requireAuth, requireAdmin],
  handlers: [
    route.get("/admin/users", { resolve: ... }),
    route.delete("/admin/users/:id", { resolve: ... })
  ]
});

GroupOptions

interface GroupOptions {
  guards: GuardFn[];
  handlers: (Handler | Handler[])[];
}

Properties:

Notes:

Validator API

Validator

interface Validator<TSchema extends SchemaLike> {
  validate<S extends TSchema>(
    schema: S,
    input: unknown,
    part: ValidationPart
  ): ValidateResult<InferSchema<S>, InferSchemaError<S>>;
}

Adapter interface for validation libraries.

Type parameters:

Methods:

validate()

validate<S extends TSchema>(
  schema: S,
  input: unknown,
  part: ValidationPart
): ValidateResult<InferSchema<S>, InferSchemaError<S>>

Parameters:

Returns: ValidateResult (success or failure)

ValidateResult

type ValidateResult<T, TErr> =
  | ValidateOk<T>
  | ValidateErr<TErr>;

ValidateOk

interface ValidateOk<T> {
  ok: true;
  value: T;
}

Properties:

ValidateErr

interface ValidateErr<TErr> {
  ok: false;
  issues: ValidationIssue[];
  error?: TErr;
}

Properties:

SchemaLike

interface SchemaLike<TOut = unknown, TErr = unknown> {
  safeParse(input: unknown): SafeParseResult<TOut, TErr>;
}

Minimal interface that validation schemas must implement.

Methods:

SafeParseResult

type SafeParseResult<T, E> =
  | SafeParseSuccess<T>
  | SafeParseFailure<E>;

SafeParseSuccess

interface SafeParseSuccess<T> {
  success: true;
  data: T;
}

SafeParseFailure

interface SafeParseFailure<E> {
  success: false;
  error: E;
}

Validator example (Zod)

import { z } from "zod";
import type { Validator, ValidationIssue } from "@hectoday/http";

export const zodValidator: Validator<z.ZodType> = {
  validate(schema, input, part) {
    const result = schema.safeParse(input);
    
    if (result.success) {
      return { ok: true, value: result.data };
    }
    
    const issues: ValidationIssue[] = result.error.issues.map(issue => ({
      part,
      path: issue.path.map(String),
      message: issue.message,
      code: issue.code
    }));
    
    return {
      ok: false,
      issues,
      error: result.error
    };
  }
};

Type inference

InferSchema

type InferSchema<T> = T extends SchemaLike<infer TOut, any> ? TOut : never;

Extract the output type from a schema.

Example:

const schema = z.object({ name: z.string() });
type Output = InferSchema<typeof schema>; // { name: string }

InferSchemaError

type InferSchemaError<T> = T extends SchemaLike<any, infer TErr> ? TErr : never;

Extract the error type from a schema.

InferInput

type InferInput<T> = T extends SchemaLike<infer TOut, any>
  ? TOut
  : T extends { safeParse: any }
  ? any
  : never;

Infer input type from schema (for type-safe handlers).

Helper recipes

Common helper patterns available as copy-paste recipes in the documentation.

Available helpers

Usage pattern

Helpers are copy-paste recipes, not dependencies:

  1. Visit the helper documentation page
  2. Copy the code to your project (e.g., helpers/maxBodyBytes.ts)
  3. Import from your project
  4. Modify as needed

Example:

// 1. Copy code from docs to helpers/maxBodyBytes.ts
// 2. Import from YOUR project
import { maxBodyBytes, SIZES } from "./helpers/maxBodyBytes.ts";

route.post("/upload", {
  guards: [maxBodyBytes(10 * SIZES.MB)],
  resolve: async (c) => {
    const data = await c.request.arrayBuffer();
    return Response.json({ size: data.byteLength });
  }
});

Why copy-paste?

See Composition Over Configuration for more details.

Constants

HTTP status codes

No built-in constants, use numbers directly:

return Response.json({ error: "Not found" }, { status: 404 });

Common status codes:

CodeMeaning
200OK
201Created
204No Content
400Bad Request
401Unauthorized
403Forbidden
404Not Found
409Conflict
422Unprocessable Entity
429Too Many Requests
500Internal Server Error
503Service Unavailable

HTTP methods

No built-in constants, use strings:

route.on("PROPFIND", "/webdav", { resolve: ... })

Error handling

Framework errors

Hectoday HTTP throws errors for:

No validator provided when schemas exist:

// Throws: "Validator is required when route defines request schemas"
const app = setup({
  handlers: [
    route.post("/users", {
      request: { body: schema }, // Schema defined
      resolve: (c) => ...
    })
  ]
  // Missing validator!
});

Solution: Provide a validator:

const app = setup({
  validator: zodValidator, // ✓ Now provided
  handlers: [...]
});

404 handling

Framework returns 404 when no route matches:

// No route for /unknown
const app = setup({ handlers: [...] });

const response = await app.fetch(new Request("http://localhost/unknown"));
// response.status === 404
// response.body === "Not Found"

Custom 404: Add a catch-all route:

route.all("/*", {
  resolve: () => Response.json({ error: "Not found" }, { status: 404 })
})

Version compatibility

Minimum runtime requirements:

Web Standard APIs required:


Next: Philosophy (revisited) - Why Hectoday HTTP makes these design choices.