OpenAPI Endpoint Documentation
The spec is generated in two passes: (1) a filesystem scanner auto-infers schemas from handler factories, (2) a merge script overlays human-authored metadata from supplement files. The full human-readable guide is at docs/OPENAPI_GUIDE.md .
Quick Start
New App Router endpoint
-
Create src/app/api/<path>/route.ts using a handler factory
-
Create src/app/api/<path>/schema.ts with InputSchema
- OutputSchema
-
Create src/lib/openapi/endpoints/<domain>/<endpoint-name>.ts (supplement)
-
Export supplement from src/lib/openapi/endpoints/<domain>/index.ts
-
Run npx tsx scripts/generate-openapi.ts
-
Verify at http://localhost:3000/api-docs
Update existing endpoint
-
Modify schema in schema.ts — auto-captured by scanner
-
Update supplement metadata/examples in src/lib/openapi/endpoints/<domain>/
-
Run npx tsx scripts/generate-openapi.ts
New edge function endpoint
-
Create registration function in src/lib/openapi/endpoints/<domain>/edge-process-imports.ts
-
Use registry.registerPath() with raw SchemaObject (not Zod)
-
Import and call from scripts/generate-openapi.ts
-
Run npx tsx scripts/generate-openapi.ts
-
Verify at http://localhost:3000/api-docs
For edge function details, see reference.md.
New App Router Endpoint
Step 1: Create route handler
Use one of the 4 handler factories from src/lib/api-helpers/create-handler.ts :
-
createGetApiHandlerWithAuth — GET with auth
-
createPostApiHandlerWithAuth — POST with auth
-
createGetApiHandler — GET without auth
-
createPostApiHandler — POST without auth
// src/app/api/<domain>/<endpoint>/route.ts import { createGetApiHandlerWithAuth } from "@/lib/api-helpers/create-handler"; import { MyInputSchema, MyOutputSchema } from "./schema";
const ROUTE = "domain-endpoint-name";
export const GET = createGetApiHandlerWithAuth({ route: ROUTE, inputSchema: MyInputSchema, outputSchema: MyOutputSchema, handler: async ({ data, supabase, user, route }) => { // business logic return { result: "value" }; }, });
Step 2: Create schema file
Colocate Zod schemas next to route.ts :
// src/app/api/<domain>/<endpoint>/schema.ts import { z } from "zod";
export const MyInputSchema = z.object({ url: z.string(), });
export const MyOutputSchema = z.object({ result: z.string(), });
Step 3: Create supplement file
Supplements provide metadata the scanner can't infer: tags, summary, description, examples.
// src/lib/openapi/endpoints/<domain>/<endpoint-name>.ts import { type EndpointSupplement } from "@/lib/openapi/supplement-types"; import { bearerAuth } from "@/lib/openapi/registry";
export const myEndpointSupplement = { path: "/<domain>/<endpoint-name>", method: "get", tags: ["<Domain>"], summary: "Short description for Scalar heading", description: "Detailed explanation of what the endpoint does.", security: [{ [bearerAuth.name]: [] }, {}], responseExample: { data: { result: "value" }, error: null, }, } satisfies EndpointSupplement;
Rules:
-
path is relative to /api (e.g., /bookmarks/check-url not /api/bookmarks/check-url )
-
method is lowercase: "get" or "post"
-
Tags are capitalized: "Bookmarks" , "Categories" , "Twitter"
-
Security [{ [bearerAuth.name]: [] }, {}] — empty {} means cookie auth also accepted
-
Export name: <camelCaseName>Supplement
Step 4: Export from barrel
// src/lib/openapi/endpoints/<domain>/index.ts export { myEndpointSupplement } from "./<endpoint-name>";
Step 5: Regenerate and verify
npx tsx scripts/generate-openapi.ts
Open http://localhost:3000/api-docs and verify the endpoint appears.
Updating an Existing Endpoint
Schema changes
Modify the Zod schema in schema.ts — the scanner picks it up automatically. The GitHub Actions changelog workflow runs oasdiff on every push to dev and appends changes to docs/API_CHANGELOG.md .
Updating supplement metadata
Edit the supplement file directly: summary , description , tags , additionalResponses .
Adding examples
Single example (simple endpoints):
responseExample: { data: { id: 1, name: "Example" }, error: null, },
Named examples (multiple scenarios — creates a dropdown in Scalar):
responseExamples: { "happy-path": { summary: "Successful response", description: "Returns the created resource.", value: { data: { id: 1 }, error: null }, }, "duplicate-detected": { summary: "Duplicate skipped", description: "URL already bookmarked.", value: { data: { inserted: 0, skipped: 1 }, error: null }, }, },
400 error examples:
response400Examples: { "empty-array": { summary: "Empty bookmarks array", description: "Fails when bookmarks array has no elements.", value: { data: null, error: "bookmarks: Array must contain at least 1 element(s)" }, }, }, additionalResponses: { 400: { description: "Invalid request body or bookmark data" }, },
When the supplement file exceeds 250 lines, extract examples to <endpoint-name>-examples.ts .
Parameter examples (GET query param dropdown in Scalar):
parameterExamples: { email: { "valid-user": { summary: "Valid user email", description: "Returns user data for this email.", value: "user@example.com", }, "unknown-email": { summary: "Nonexistent email", description: "Returns null/empty result.", value: "nobody@example.com", }, }, },
Outer key = parameter name (must match name in the generated spec). Inner map = standard named examples. Creates a dropdown per query param in Scalar's "Try It" panel.
Supplement Reference
EndpointSupplement fields
Field Type Purpose
path
string
Route path relative to /api (required)
method
string
"get" or "post" lowercase (required)
tags
string[]
Scalar sidebar grouping
summary
string
Short heading (one line)
description
string
Detailed explanation (supports markdown)
security
Array<Record<string, string[]>>
Auth requirements
requestExample
Record<string, unknown>
Single request body example
requestExamples
Named examples object Multiple request examples
responseExample
Record<string, unknown>
Single 200 response example
responseExamples
Named examples object Multiple 200 response examples
response400Example
Record<string, unknown>
Single 400 error example
response400Examples
Named examples object Multiple 400 error examples
additionalResponses
Record<number, { description }>
Extra response codes (400, 403, 404)
parameterExamples
Record<string, NamedExamples>
Per-param named examples (GET dropdown)
Naming conventions
-
Export name: <camelCaseName>Supplement (e.g., checkUrlSupplement )
-
Named example keys: kebab-case ("single-tweet" , "validation-error" )
-
Example files: <endpoint-name>-examples.ts (e.g., sync-examples.ts )
-
Tags: capitalized ("Bookmarks" , "iPhone" )
-
All named examples require both summary and description
-
Happy paths first, then validation errors
Response components
The scanner registers 3 $ref response components for every endpoint:
-
ValidationError (400) — { data: null, error: string }
-
Unauthorized (401) — { data: null, error: "Not authenticated" }
-
InternalError (500) — { data: null, error: "Failed to process request" }
additionalResponses overrides the 400 description while preserving the schema.
Agent Prompt Templates
Create supplement for a new endpoint
I just created a new App Router endpoint at src/app/api/<path>/route.ts with schema at src/app/api/<path>/schema.ts . Create the OpenAPI supplement file, export it from the barrel, regenerate the spec, and verify at /api-docs .
Domain: <bookmarks|categories|tags|twitter|instagram|raindrop|profiles|iphone>
Update examples for an existing endpoint
Update the OpenAPI examples for the <endpoint-name> endpoint. Read the current supplement at src/lib/openapi/endpoints/<domain>/<endpoint-name>.ts , add named examples for these scenarios: [describe scenarios]. Regenerate the spec and verify.
Add 400 error examples
Add response400Examples to the <endpoint-name> supplement. Include examples for: [list validation errors]. Also add additionalResponses: { 400: { description: "<custom description>" } } . Regenerate and verify.
Add parameter examples for a GET endpoint
Add parameterExamples to the <endpoint-name> supplement for each query parameter. Include examples for: [list test scenarios per param]. Regenerate the spec and verify the dropdown appears in Scalar's "Try It" panel.
Document a new edge function
I created a new Supabase Edge Function at supabase/functions/<name>/ . Register it in the OpenAPI spec following the pattern in src/lib/openapi/endpoints/instagram/edge-process-imports.ts . Use serviceRoleAuth , edgeFunctionServers , and raw SchemaObject . Wire it into scripts/generate-openapi.ts .
Troubleshooting
Supplement not appearing in spec
-
Is the supplement exported from the domain's index.ts barrel?
-
Does path match the route path exactly (relative to /api , no trailing slash)?
-
Does method match the exported handler ("get" for GET , "post" for POST )?
-
Check console output — mergeSupplements prints warnings for unmatched supplements
400 examples not showing
-
Does the supplement have additionalResponses: { 400: { description: "..." } } ?
-
Are named examples using response400Examples (not responseExamples )?
-
The merge script initializes 400 content automatically when examples are provided
Common mistakes
-
Using /api/bookmarks/check-url instead of /bookmarks/check-url for the path
-
Forgetting as const on example data objects in -examples.ts files
-
Using responseExample (singular) when you need responseExamples (named/plural)
-
Missing summary or description on named examples — both are required
For edge function patterns and complex examples, see reference.md.