.claude/skills/openapi-endpoints/SKILL.md
openapi-endpoints
npx skillsauth add timelessco/recollect openapi-endpointsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill runs an end-to-end lab: discover → audit schemas → create supplement → verify. Execute all 6 phases autonomously. 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.
Phase 1 Phase 2 Phase 3 Phase 4 Phase 5 Phase 6
DISCOVER → SCHEMA AUDIT → SUPPLEMENT → BARREL → VERIFY SPEC → VERIFY UI
↑ |
└──────────── fix and retry if any verification fails ─────────┘
Gather all context autonomously. Never ask the caller for file paths.
Glob src/app/api/**/<endpoint-name>/route.ts
Read it. Identify:
src/lib/api-helpers/create-handler.ts): createGetApiHandlerWithAuth / createPostApiHandlerWithAuth (auth required), createGetApiHandler / createPostApiHandler (no auth)src/lib/api-helpers/create-handler-v2.ts): withAuth (auth required), withPublic (no auth), always wrapped as createAxiomRouteHandler(withAuth/withPublic({...})). Handler .config has { auth, contract: "v2", factoryName: "withAuth"|"withPublic", inputSchema, outputSchema, route }create-handler-v2.ts, note it as v2 — this affects response example format (see Phase 3)export const GET or export const POST)InputSchema/OutputSchema, or imported from ./schemaCheck for colocated schema.ts first:
Glob src/app/api/**/<endpoint-name>/schema.ts
If no schema.ts exists, schemas are inline in route.ts. Note this for Phase 2.
The domain is the first path segment after /api/ (e.g., instagram, profiles, twitter):
Read src/lib/openapi/endpoints/<domain>/index.ts
Pick any existing supplement in the same domain directory:
Glob src/lib/openapi/endpoints/<domain>/*.ts
Read one (not index.ts, not *-examples.ts, not edge-process-imports.ts). This shows the exact pattern to follow — tags, security, example format.
<camelCaseDomainAndEndpoint>Supplement (e.g., instagramLastSyncedIdSupplement)<endpoint-name>.ts matching the route directory name/<domain>/<endpoint-name> (relative to /api, no /api prefix, no trailing slash)["Instagram"], ["Profiles"])Every Zod schema field must have .meta({ description: "..." }). This maps directly to field
descriptions in the generated OpenAPI spec via @asteasolutions/zod-to-openapi. Without it,
fields appear in the spec with no description — bad developer experience.
Read the schema (from schema.ts or inline in route.ts). For every field in both
InputSchema and OutputSchema, check for .meta({ description: "..." }).
.meta()For any field without .meta(), add it:
// Before
z.string()
// After
z.string().meta({ description: "The ID of the last synced Instagram bookmark" })
For nested objects and arrays:
z.array(z.int()).meta({ description: "Updated ordered list of favorite category IDs" })
z.object({
id: z.string().meta({ description: "Tag identifier" }),
name: z.string().meta({ description: "Tag display name" }),
}).meta({ description: "The newly created tag" })
schema.ts if neededIf schemas are defined inline in route.ts and don't already have a schema.ts file, extract
them to a colocated schema.ts. Follow this pattern:
// src/app/api/<domain>/<endpoint>/schema.ts
import { z } from "zod";
export const <PascalCase>InputSchema = z.object({
field: z.string().meta({ description: "Field description" }),
});
export const <PascalCase>OutputSchema = z.object({
field: z.string().meta({ description: "Field description" }),
});
Then update route.ts to import from ./schema instead of defining inline.
.meta() styleRead these files for well-documented schema patterns:
src/app/api/category/delete-user-category/schema.ts — 11-field response, boolean defaultsrc/app/api/tags/create-and-assign-tag/schema.ts — nested sub-schemas with independent .meta()src/app/api/bookmark/fetch-discoverable-by-id/schema.ts — deeply nested with MetadataSchemaUse this template. Fill in applicable fields, delete inapplicable ones:
/**
* @module Build-time only
*/
import { type EndpointSupplement } from "@/lib/openapi/supplement-types";
import { bearerAuth } from "@/lib/openapi/registry";
export const <camelCaseName>Supplement = {
path: "/<domain>/<endpoint-name>",
method: "<get|post>",
tags: ["<Domain>"],
summary: "<One-line summary for Scalar heading>",
description: "<Detailed explanation. Supports markdown.>",
security: [{ [bearerAuth.name]: [] }, {}],
// --- Request examples (POST only) ---
// Single: requestExample: { field: "value" },
// Named: requestExamples: { "key": { summary, description, value } },
// --- Response examples ---
// Single: responseExample: { data: { ... }, error: null },
// Named: responseExamples: { "key": { summary, description, value } },
// --- Error examples ---
// response400Examples: { "key": { summary, description, value: { data: null, error: "..." } } },
// --- Additional response codes ---
// additionalResponses: { 400: { description: "..." } },
// --- Parameter examples (GET only) ---
// parameterExamples: { paramName: { "key": { summary, description, value } } },
} satisfies EndpointSupplement;
path relative to /api — NOT /api/bookmarks/check-url, just /bookmarks/check-urlmethod lowercase: "get" or "post""Bookmarks", "Categories", "Twitter", "iPhone"[{ [bearerAuth.name]: [] }, {}] — {} means cookie auth also acceptedsecurity: []<camelCaseName>Supplement/** @module Build-time only */{ data: ..., error: null } wrapper
responseExample: { data: { hasApiKey: true }, error: null }{data, error} envelope
responseExample: { hasApiKey: true } (or value: { hasApiKey: true } as const in named examples)summary and description required<endpoint-name>-examples.ts with as const| Scenario | Use |
|----------|-----|
| One obvious happy path | responseExample (singular) |
| Multiple success scenarios | responseExamples (named, creates dropdown in Scalar) |
| Endpoint can return validation errors | Add response400Examples + additionalResponses: { 400 } |
| GET with query params | Add parameterExamples (creates dropdown per param in "Try It") |
Read the domain barrel at src/lib/openapi/endpoints/<domain>/index.ts. Add the new export
in alphabetical order among existing exports:
export { <camelCaseName>Supplement } from "./<endpoint-name>";
The collectSupplements() function in the merge script auto-discovers any export with path
and method properties from these barrels — no registration needed beyond the barrel export.
This phase does NOT require a running dev server. These are all build-time operations.
npx tsx scripts/generate-openapi.ts
Check the output line: Supplements applied: X/Y. Verify X increased by 1 compared to before.
If X < Y, a supplement path or method doesn't match — check Phase 3 rules.
cat public/openapi.json | jq '.paths["/<domain>/<endpoint-name>"].<method> | {summary, tags, description}'
All three fields should be non-null and match what you wrote in the supplement.
pnpm fix
pnpm lint:types
If either fails, fix and re-run from 5a.
lsof -i :3000
If no process on port 3000, start the dev server:
pnpm dev &
Wait for it to be ready (check with curl -s http://localhost:3000 > /dev/null).
Use Chrome MCP to navigate to http://localhost:3000/api-docs. Search or scroll to find the
endpoint under its tag group. Confirm:
.meta() appear in the schema viewerIf the endpoint doesn't appear, re-run Phase 5a and check for merge warnings.
For updates, start at Phase 2 (schema audit) and run through Phase 6. Common updates:
Modify Zod schema in schema.ts — scanner picks it up automatically. Ensure all new fields
have .meta({ description }).
Edit the supplement file. Use named examples for multiple scenarios.
Add response400Examples and additionalResponses: { 400: { description } }.
| Field | Type | When to use |
| --------------------- | --------------------------------- | ------------------------------------------------- |
| path | string | Always (required) |
| method | string | Always (required) |
| tags | string[] | Always — groups endpoint in Scalar sidebar |
| summary | string | Always — one-line heading |
| description | string | Always — detailed explanation, supports markdown |
| security | Array<Record<string, string[]>> | Always — auth requirements |
| requestExample | Record<string, unknown> | POST with one obvious request body |
| requestExamples | Named examples | POST with multiple request scenarios |
| responseExample | Record<string, unknown> | One obvious success response |
| responseExamples | Named examples | Multiple success scenarios (dropdown in Scalar) |
| response400Example | Record<string, unknown> | One obvious validation error |
| response400Examples | Named examples | Multiple validation error scenarios |
| additionalResponses | Record<number, { description }> | Custom descriptions for 400/403/404/409/500 |
| parameterExamples | Record<string, NamedExamples> | GET endpoints with query params (dropdown per param) |
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.
<camelCaseName>Supplement (e.g., checkUrlSupplement)"single-tweet", "validation-error")<endpoint-name>-examples.ts (use as const)"Bookmarks", "iPhone")summary and descriptionEdge functions use a different workflow (manual registerPath() with raw SchemaObject).
See reference.md for the complete pattern.
index.ts barrel?path matches route exactly (relative to /api, no trailing slash)?method matches handler export ("get" for GET, "post" for POST)?mergeSupplements prints warnings for unmatched supplementsadditionalResponses: { 400: { description } }?response400Examples (not responseExamples)?/api/bookmarks/check-url instead of /bookmarks/check-urlas const on example data in -examples.ts filesresponseExample (singular) when you need responseExamples (named)summary or description on named examples.meta({ description }) on schema fields — run Phase 2 againtesting
v2-route-audit
tools
release
development
recollect-mutation-hook-refactoring
databases
recollect-caller-migration