Files
skills/docs/api-stack/api/api-router/src/route-options/route-options.md
2026-05-29 08:20:56 +01:00

3.4 KiB

route-options

(no submodules)

Purpose

The route-options module is the Zod-validated boundary for every per-route configuration block. It widens the minimal RouteOptions type from @suaveplan/api-types with the policy switches every Stage-1-and-later adapter must honour: runtime Zod validation toggle, OpenAPI inclusion, streaming mode, CSRF, idempotency, and a discriminated rate-limit shape.

Centralising the schema here lets adapters, plugin-loader, and the host all parse the same shape, so a route option declared in a plugin manifest, a TypeScript builder, or a JSON config file gets the same defaults.

Features

  • routeOptionsSchema — z.strictObject rejecting unknown keys.
  • Defaults: runtimeValidation: true, openapi: true, streaming: false.
  • request.body / request.params / request.query accept any Zod schema (structural check — no instanceof, since two z instances in the same process may not share constructors).
  • responses is a Record<status, { body: ZodSchema }> so adapters can map each documented status to its validator.
  • rateLimit is a discriminated union: either { rpm } or { rps }.

Usage: Basic

import { parseRouteOptions } from "@suaveplan/api-router/route-options";

const opts = parseRouteOptions({ auth: "users.read", summary: "List users" });
// opts.runtimeValidation === true (default applied)
// opts.openapi === true
// opts.streaming === false

Usage: Advanced

import z from "zod";
import { parseRouteOptions } from "@suaveplan/api-router/route-options";

const opts = parseRouteOptions({
    auth: "users.write",
    summary: "Create user",
    tags: ["users", "admin"],
    request: {
        body: z.object({ name: z.string(), email: z.email() }),
        query: z.object({ dryRun: z.boolean().optional() }),
    },
    responses: {
        201: { body: z.object({ id: z.string() }) },
        409: { body: z.object({ code: z.string() }) },
    },
    streaming: false,
    csrf: true,
    idempotent: true,
    rateLimit: { rpm: 60 },
});

API Reference

  • routeOptionsSchema: z.ZodObject<…> — the validated schema. Use .parse(...), .safeParse(...), or .shape for adapter-side introspection.
  • parseRouteOptions(input: unknown): RouteOptions — throw-on-fail.
  • safeParseRouteOptions(input: unknown): ZodSafeParseResult<RouteOptions> — non-throwing variant.
  • RouteOptions — z.output of the schema (post-defaults shape).
  • RouteOptionsInput — z.input (caller-supplied shape).

Implementation Notes

The schema uses z.custom with a structural predicate (looking for safeParse / _zod) rather than instanceof z.ZodType so plugins that import zod through their own dependency cone still pass the check. This is deliberate: two copies of zod in the dependency graph would otherwise break instanceof-based validation in surprising ways.

z.strictObject rejects unknown keys so typos in a plugin's route options surface at registration time, not at the first failing request. Adapters that want a permissive parse can call routeOptionsSchema.passthrough() instead — but the default contract is strict.

The rateLimit union is intentionally narrow: per-minute and per-second are the two real-world axes. Other windows (per-hour, per-day) belong in dedicated rate-limit plugins, not in the route schema.