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:
request: Request- The original Web Standard Request objectraw: RawValues- Extracted but unvalidated inputsinput: InputState- Validation results (ok or not ok)locals: Record<string, unknown>- Request-scoped data from hooks and guards
Type parameters:
TParams- Type of validated params (inferred from schema)TQuery- Type of validated query (inferred from schema)TBody- Type of validated body (inferred from schema)
RawValues
Extracted inputs from the request, not validated.
interface RawValues {
params: Record<string, string | undefined>;
query: Record<string, string | string[] | undefined>;
body?: unknown;
}Properties:
params- Path parameters from URL pattern (e.g.,:id)query- Query parameters from search stringbody- Parsed body (only if body schema defined), otherwise undefined
Notes:
- All params are
string | undefined - Query values can be arrays if parameter appears multiple times
- Body is parsed as JSON when body schema is defined
- Raw values are not type-safe, validate them
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:
ok: true- Validation passedparams- Validated, typed paramsquery- Validated, typed querybody- Validated, typed body
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:
ok: false- Validation failedfailed- Which parts failed (["params"],["query", "body"], etc.)issues- Normalized array of all validation issuesreceived- Raw values that failed validationerrors- Original error objects from validator (optional)
ValidationIssue
Normalized validation error:
interface ValidationIssue {
part: ValidationPart;
path: readonly string[];
message: string;
code?: string;
}Properties:
part- Which part failed:"params","query", or"body"path- Path to the failing field (e.g.,["email"]or["user", "name"])message- Human-readable error messagecode- Optional error code from validator
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:
method- HTTP method(s):"GET","POST", or["GET", "POST"]path- URL pattern with optional parameters:"/users/:id"handler- The function that returns a Responseguards- Optional guards that run before handlerrequest- Optional validation schemas
Notes:
- Created by
route.get(),route.post(), etc. - You rarely construct this manually
- Passed to
setup()in thehandlersarray
HandlerFn
The function that handles the request:
type HandlerFn<TParams = unknown, TQuery = unknown, TBody = unknown> = (
c: Context<TParams, TQuery, TBody>
) => Response | Promise<Response>;Parameters:
c: Context- The request context
Returns:
Response | Promise<Response>- Must return a Web Standard Response
Notes:
- Must always return a Response
- Can be async
- Type parameters inferred from schemas
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:
- Automatically inferred by TypeScript
- Used internally for type safety
- You rarely use this explicitly
Route functions
route.get()
function get<TPath extends string>(
path: TPath,
config: RouteConfig
): HandlerCreate a GET route.
Parameters:
path- URL pattern (e.g.,"/users/:id")config- Route configuration
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
): HandlerCreate a POST route.
route.put()
function put<TPath extends string>(
path: TPath,
config: RouteConfig
): HandlerCreate a PUT route.
route.patch()
function patch<TPath extends string>(
path: TPath,
config: RouteConfig
): HandlerCreate a PATCH route.
route.delete()
function delete<TPath extends string>(
path: TPath,
config: RouteConfig
): HandlerCreate a DELETE route.
route.head()
function head<TPath extends string>(
path: TPath,
config: RouteConfig
): HandlerCreate a HEAD route.
route.options()
function options<TPath extends string>(
path: TPath,
config: RouteConfig
): HandlerCreate an OPTIONS route.
route.all()
function all<TPath extends string>(
path: TPath,
config: RouteConfig
): HandlerCreate a route that matches all HTTP methods.
route.on()
function on<TPath extends string>(
method: string,
path: TPath,
config: RouteConfig
): HandlerCreate a route for a custom HTTP method.
Parameters:
method- Any HTTP method string (e.g.,"PROPFIND")path- URL patternconfig- Route configuration
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:
request- Optional validation schemasguards- Optional guardsresolve- Handler function (required)
RequestSchemas
interface RequestSchemas<
TParamsSchema = unknown,
TQuerySchema = unknown,
TBodySchema = unknown
> {
params?: TParamsSchema;
query?: TQuerySchema;
body?: TBodySchema;
}Properties:
params- Schema for path parametersquery- Schema for query stringbody- Schema for request body
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:
config: Config | Handler[]- Configuration object or array of handlers
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:
handlers- Array of route handlers (required)validator- Validator adapter (required if any route uses schemas)onRequest- Hook that runs before routingonResponse- Hook that runs after handleronError- Hook that handles unexpected errors
OnRequestHandler
type OnRequestHandler = (
info: { request: Request }
) => void | Record<string, unknown> | Promise<void | Record<string, unknown>>;Runs before routing, receives the raw Request.
Parameters:
info.request: Request- The incoming request
Returns:
void- No locals to addRecord<string, unknown>- Locals to merge into contextPromise- Async version of above
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:
- Cannot deny requests
- Cannot return Response
- Only adds to
c.locals
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:
info.context: Context- The request contextinfo.response: Response- The response from handler
Returns:
Response- Modified or original response
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:
info.error: unknown- The thrown errorinfo.context: Context- Minimal context (might not have full data)
Returns:
Response- Error response to send to client
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:
- Only catches unexpected errors (bugs, crashes)
- Expected errors should be returned explicitly in handlers
- Don’t expose error details to clients in production
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:
c: Context- Request context
Returns:
GuardResult- Allow or deny
Notes:
- Can be async
- Must return
GuardResult - Never throws (to deny, return
{ deny: Response })
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:
allow: true- Request continueslocals- Optional data to add toc.locals
Example:
return { allow: true, locals: { userId: "123" } };Deny result
{ deny: Response }Properties:
deny: Response- The response to send (request ends)
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:
options: GroupOptions- Group configuration
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:
guards- Guards to apply to all handlershandlers- Handlers (or nested groups) to apply guards to
Notes:
- Guards are prepended to each handler’s guards
- Nested groups accumulate guards
- Operates at build time (no runtime overhead)
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:
TSchema- The schema type from your validation library
Methods:
validate()- Validate input against schema
validate()
validate<S extends TSchema>(
schema: S,
input: unknown,
part: ValidationPart
): ValidateResult<InferSchema<S>, InferSchemaError<S>>Parameters:
schema- The schema to validate againstinput- The data to validate (unknown type)part- Which part is being validated ("params","query","body")
Returns: ValidateResult (success or failure)
ValidateResult
type ValidateResult<T, TErr> =
| ValidateOk<T>
| ValidateErr<TErr>;ValidateOk
interface ValidateOk<T> {
ok: true;
value: T;
}Properties:
ok: true- Validation succeededvalue: T- The validated, typed value
ValidateErr
interface ValidateErr<TErr> {
ok: false;
issues: ValidationIssue[];
error?: TErr;
}Properties:
ok: false- Validation failedissues- Normalized array of issueserror- Optional original error from validator
SchemaLike
interface SchemaLike<TOut = unknown, TErr = unknown> {
safeParse(input: unknown): SafeParseResult<TOut, TErr>;
}Minimal interface that validation schemas must implement.
Methods:
safeParse()- Parse input, return success or failure
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
- Zod validator - Validator adapter for Zod schemas
- maxBodyBytes - Limit request body size (guard)
- CORS - Add CORS headers to responses
- Request ID - Generate and track request IDs
- Rate limiting - Limit requests per client
Usage pattern
Helpers are copy-paste recipes, not dependencies:
- Visit the helper documentation page
- Copy the code to your project (e.g.,
helpers/maxBodyBytes.ts) - Import from your project
- 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?
- You own the code - No external dependencies
- No version conflicts - No need to track updates
- Modify freely - Change without forking
- Copy only what you need - No bloat
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:
| Code | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 204 | No Content |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 409 | Conflict |
| 422 | Unprocessable Entity |
| 429 | Too Many Requests |
| 500 | Internal Server Error |
| 503 | Service 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:
- Deno 1.30+
- Bun 1.0+
- Node.js 18+ (with fetch support)
- Cloudflare Workers (any version with Request/Response)
Web Standard APIs required:
RequestResponseHeadersURLPattern(for route matching)crypto.randomUUID()(for request IDs in helpers)
Next: Philosophy (revisited) - Why Hectoday HTTP makes these design choices.