add-openapi
Skill106 starsChanged 7 months agoMITView on GitHub
Wire `@pracht/openapi`: generate an OpenAPI 3.1 document from API routes, add response contracts, serve a Scalar or Swagger UI, and gate completeness in CI. Use for "add OpenAPI", "generate an API spec", "add Swagger", "publish API docs".
- Installs packages
In this repository33 other files it gives its agents.
- AGENTS.mdAGENTS.md
- add-authSkill
- add-capabilitiesSkill
- add-contentSkill
- add-dbSkill
- add-i18nSkill
- add-imagesSkill
- add-observabilitySkill
- audit-a11ySkill
- audit-agent-surfaceSkill
- audit-authSkill
- audit-bundlesSkill
- audit-csrfSkill
- audit-depsSkill
- audit-headersSkill
- audit-islandsSkill
- audit-loadersSkill
- audit-redirectsSkill
- audit-secretsSkill
- audit-seoSkill
- audit-shellsSkill
- configure-isgSkill
- migrate-nextjsSkill
- pracht-debugSkill
- pracht-deploySkill
- pracht-scaffoldSkill
- pracht-test-apiSkill
- pre-deploySkill
- scaffold-e2eSkill
- scaffold-testsSkill
- tune-render-modeSkill
- typed-routesSkill
- upgrade-prachtSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 3 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 42
Ask the user whether the document should be public, and whether they want a reference UI at all (ui: false is the default).
- Distinctive: line 191
Treat the document and UI as public unless the hosting layer protects the emitted static files.
- Distinctive: line 194
Never embed an OAuth client secret in UI configuration — browser-delivered configuration is observable by every visitor.
- Distinctive: line 196
Keep the UI and JSON on the same origin.
- Very specific: line 202
Do not hand-document a capability HTTP projection with defineOpenApi() — set capabilities: true and the plugin documents every expose.http …
- Very specific: line 205
Never overwrite an existing vite.config.ts or a hand-written public/openapi.json — diff first and confirm with AskUserQuestion.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Pracht Add OpenAPI
@pracht/openapi is an opt-in companion package (https://pracht.resynapse.dev/docs/openapi). Ordinary
defineApi() routes keep their authoring, runtime behavior, and apiFetch()
type inference — the package only reads the resolved server graph and owns its
own descriptor.
Step 1: Inventory the API first
MCP: if pracht dev-mcp is registered, prefer its inspect_*, doctor, and verify tools to the CLI.
pracht inspect api --json # endpoint paths, methods, hasDefaultHandlerNote two things before generating anything:
- Default-export handlers are not expanded — their supported methods cannot be inferred honestly. Split them into named method exports if they belong in the document.
- Catch-all paths become a single
{path}parameter and emit a warning about slash encoding.
Ask the user whether the document should be public, and whether they want a
reference UI at all (ui: false is the default).
Step 2: Install and register the plugin
pnpm add @pracht/openapi// vite.config.ts — the companion plugin goes after pracht()
import { prachtOpenApi } from "@pracht/openapi/vite";
import { pracht } from "@pracht/vite-plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
pracht(),
prachtOpenApi({
info: {
title: "Acme API",
version: "1.0.0",
description: "Public HTTP API for Acme.",
},
ui: "scalar", // or "swagger", an object, or omit for no UI
}),
],
});This reserves /openapi.json and (with a UI) /docs. Both are live in the
Vite dev server; pracht build writes dist/client/openapi.json and
dist/client/docs/index.html, which Node, Cloudflare, Netlify, and Vercel serve
through their existing static-asset paths. Paths are configurable via
documentPath and ui.path; the document path must end in .json so
static hosts assign the right media type.
Reserved paths shadow app routes: development logs a warning on a collision and
production generation replaces the colliding public/build file and reports the
replacement. Grep the manifest and public/ for /docs before enabling the UI
on an app that already documents itself there.
Step 3: Attach the metadata that cannot be inferred
Paths, methods, path params, and convertible request validators come from the
routes. Status codes and payload shapes cannot be recovered from an arbitrary
Response or an erased TypeScript type — declare them:
import { defineApi, json } from "@pracht/core";
import { defineOpenApi } from "@pracht/openapi";
export const POST = defineOpenApi(
defineApi({
body: createItemSchema,
handler: ({ body }) => json({ id: createItem(body) }, { status: 201 }),
}),
{
operationId: "createItem",
summary: "Create an item",
tags: ["items"],
responses: {
201: { description: "Item created", body: itemSchema },
},
},
);defineOpenApi() mutates and returns the same handler — validation, dispatch,
and client inference stay with pracht. Generation adds the framework-known 400
and 422 validation responses itself, marks a request body optional when the
validator accepts the empty-body undefined, and emits a valid undocumented
default response plus a scoped warning when the contract is unknown.
Step 4: Shared document metadata
prachtOpenApi({
info: { title: "Acme API", version: "1.0.0" },
document: {
servers: [
{ url: "https://api.example.com", description: "Production" },
{ url: "http://localhost:3000", description: "Local development" },
],
tags: [{ name: "items", description: "Item lifecycle" }],
components: {
securitySchemes: {
bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "JWT" },
},
},
security: [{ bearerAuth: [] }],
},
});A security scheme referenced by an operation's security must exist in
document.components.securitySchemes. document.security applies globally; set
security: [] on an operation to mark it public.
Deploy base: with Vite base: "/app/", the UI loads its document from
/app/openapi.json, and generation adds servers: [{ url: "/app" }] when
document.servers is omitted so "Try it out" reaches base-prefixed routes. An
explicit document.servers always wins, including an empty array.
Step 5: Make it a CI gate
Conversion failures are warnings by default, so one unsupported handler does not erase the document. Once every public operation has an explicit response contract, tighten it:
prachtOpenApi({ info: { title: "Acme API", version: "1.0.0" }, failOnWarnings: true });Any warning then fails the dev request and pracht build. There is no dedicated
diff/check command; deterministic build output plus failOnWarnings catches
drift.
Step 6: UI provider, CDN, and CSP
ui: "scalar"— modern reference with request examples and an API client.ui: "swagger"— Swagger UI, with deep links on and the remote validator disabled so internal documents are never sent tovalidator.swagger.io.- Both shells load pinned browser assets from jsDelivr. Apps with offline
requirements or a no-third-party-CDN policy should self-host and override
(
scriptUrlfor both providers,styleUrlfor Swagger), copying the pinned distribution intopublic/vendor/. - The shell also contains a small inline bootstrap script. A strict CSP must
allow that exact script by hash (and the asset origin), or the page must be
replaced with an app-owned route following the app's nonce policy —
self-hosting the bundle alone does not fix an inline-script ban. See
https://pracht.resynapse.dev/docs/recipes/csp and
/audit-headers.
Step 7: Verify
pracht dev # GET /openapi.json and the UI path
pracht build
pracht verify --jsonConfirm the document parses, every intended operation is present with a real
response contract, and no 500-only default responses remain for public
endpoints. Then run /audit-headers if a CSP is in place and
/audit-agent-surface if the document is part of a deliberate agent-facing
surface.
Rules
- Treat the document and UI as public unless the hosting layer protects the emitted static files. No secrets, internal hostnames, or credentials in descriptions or examples.
- Never embed an OAuth client secret in UI configuration — browser-delivered configuration is observable by every visitor.
- Keep the UI and JSON on the same origin.
- "Try it out" sends real requests: mutation endpoints must carry the same authentication, authorization, CSRF, rate-limit, and confirmation policies as any other client.
- Set a sane static cache policy for the document; immutable caching is wrong unless the URL is versioned or deployments purge it.
- Do not hand-document a capability HTTP projection with
defineOpenApi()— setcapabilities: trueand the plugin documents everyexpose.httpcapability from its own schemas (/add-capabilitiesowns that contract). - Never overwrite an existing
vite.config.tsor a hand-writtenpublic/openapi.json— diff first and confirm withAskUserQuestion.
$ARGUMENTS
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

