api-specs-openapi
OpenAPI 3.1 specification, schema design, code generation
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-specs-openapi/skills/api-specs-openapi
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
OpenAPI Specification Patterns
Quick Guide: Use OpenAPI 3.1 for API contracts. 3.1 is a superset of JSON Schema Draft 2020-12 -- use
type: ["string", "null"]instead ofnullable: true. Define all reusable schemas incomponents/schemasand reference with$ref. Always includeoperationIdon every operation (it becomes the client method name). Useopenapi-typescriptto generate zero-runtime TypeScript types andopenapi-fetchfor a 6kb type-safe fetch client.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use OpenAPI 3.1 syntax -- type: ["string", "null"] NOT the 3.0 nullable: true keyword)
(You MUST define reusable schemas in components/schemas and reference with $ref -- NO inline schema duplication)
(You MUST include operationId on every path operation -- it becomes the generated client method name)
(You MUST use openapi-typescript for type generation and import types with import type -- types are zero-runtime)
</critical_requirements>
Auto-detection: OpenAPI, openapi, swagger, openapi-typescript, openapi-fetch, createClient, paths, components, schemas, operationId, $ref, discriminator, oneOf, allOf, anyOf, openapi: "3.1", spec-first, API contract, API specification, code generation, schema design
When to use:
- Defining API contracts before or alongside implementation (spec-first or code-first)
- Generating TypeScript types from an existing OpenAPI spec
- Building type-safe API clients with automatic request/response validation
- Documenting REST APIs for external or internal consumers
- Designing reusable schema components with
$refcomposition
When NOT to use:
- Internal-only endpoints with no external consumers and no documentation needs
- GraphQL APIs (use GraphQL schema tooling instead)
- Simple scripts or prototypes where formal contracts add overhead
Key patterns covered:
- OpenAPI 3.1 spec structure (info, paths, components, servers)
- Schema design with JSON Schema Draft 2020-12 alignment
$refcomposition,oneOf/allOf/anyOf, discriminators- Path operations with parameters, request bodies, and responses
- TypeScript type generation with
openapi-typescriptv7 - Type-safe fetch client with
openapi-fetch - Spec-first vs code-first decision framework
Detailed Resources:
- examples/core.md - Spec structure, schemas, paths, operations,
$refcomposition - examples/codegen.md - TypeScript type generation,
openapi-fetchclient - examples/validation.md - Request/response validation, middleware patterns
- reference.md - Decision frameworks, anti-patterns, quick-lookup tables
<decision_framework>
Decision Framework
Spec-First vs Code-First
Is this a public or multi-consumer API?
|-- YES --> Spec-first (design contract, get feedback, then implement)
+-- NO --> Is this a rapid prototype?
|-- YES --> Code-first (generate spec from annotations)
+-- NO --> Does your framework generate OpenAPI from code?
|-- YES --> Code-first (framework handles spec generation)
+-- NO --> Spec-first (write the YAML, generate types)
Schema Composition
Need to share fields across schemas?
|-- YES --> allOf with a base $ref schema
+-- NO --> Need polymorphism (multiple possible shapes)?
|-- YES --> oneOf with discriminator
+-- NO --> Need to combine constraints?
|-- YES --> allOf (all must match)
+-- NO --> Simple schema with $ref for nested objects
Type Generation Tooling
| Need | Tool | When |
|---|---|---|
| TypeScript types from spec | openapi-typescript v7 |
Always -- zero-runtime type generation |
| Type-safe fetch client | openapi-fetch |
Frontend or service-to-service calls |
| Full SDK generation | @hey-api/openapi-ts |
Need Zod schemas, query hooks, or full SDKs |
| Spec validation/linting | Redocly CLI | CI pipeline, pre-commit checks |
</decision_framework>
<red_flags>
RED FLAGS
High Priority:
- Using
nullable: true-- removed in OpenAPI 3.1, usetype: ["string", "null"] - Duplicating schemas inline instead of using
$reftocomponents/schemas-- creates drift - Missing
operationIdon operations -- generated clients get ugly auto-generated names - Manually maintaining TypeScript interfaces that mirror the spec -- use
openapi-typescriptto generate them - Using
openapi: "3.0.x"when 3.1 is available -- misses JSON Schema alignment
Medium Priority:
- Defining error responses without a shared
Errorschema -- inconsistent error shapes across endpoints - Missing
requiredarray on object schemas -- all properties become optional by default - Using
type: objectwithoutadditionalProperties: falsewhen extra fields should be rejected - Not documenting
4xx/5xxresponses -- consumers don't know what error shapes to expect
Gotchas & Edge Cases:
$refsiblings are ignored in 3.0 but allowed in 3.1 --descriptionnext to$refnow works in 3.1exclusiveMinimum/exclusiveMaximumchanged from boolean (3.0) to number (3.1) --exclusiveMinimum: 0means "greater than 0"discriminatordoes not affect validation -- it's a hint for code generators, not a constraintdiscriminator.propertyNamemust be a required string property at the same schema level- Inline schemas inside
discriminatoroneOfare not considered -- only$refentries work openapi-typescriptgenerates.d.tsfiles -- these are type-only, no runtime codeopenapi-fetchdatais only present for 2xx responses,errorfor 4xx/5xx -- always check which is defined- Response bodies are consumed once -- clone the response if middleware needs to read it and pass it through
openapi-typescriptv7 usesredocly.yamlfor multi-schema config -- globbing is deprecated
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST use OpenAPI 3.1 syntax -- type: ["string", "null"] NOT the 3.0 nullable: true keyword)
(You MUST define reusable schemas in components/schemas and reference with $ref -- NO inline schema duplication)
(You MUST include operationId on every path operation -- it becomes the generated client method name)
(You MUST use openapi-typescript for type generation and import types with import type -- types are zero-runtime)
Failure to follow these rules will cause schema drift, broken codegen, and type mismatches between spec and implementation.
</critical_reminders>
Files (skills)
-
examples
-
codegen.md 8 KB
# TypeScript Code Generation > Related: [core.md](core.md) for spec structure, [validation.md](validation.md) for request/response validation --- ## Pattern 1: CLI Type Generation with openapi-typescript Generate zero-runtime TypeScript types from your OpenAPI spec. ### Basic Usage ```bash # From local file npx openapi-typescript ./api/openapi.yaml -o ./api/schema.d.ts # From remote URL npx openapi-typescript https://api.example.com/openapi.json -o ./api/schema.d.ts # With useful flags npx openapi-typescript ./api/openapi.yaml -o ./api/schema.d.ts \ --immutable \ # Generate readonly properties and arrays --alphabetize \ # Sort types alphabetically --export-type # Use 'type' instead of 'interface' ``` ### Useful CLI Flags | Flag | Default | Purpose | | ------------------------ | ------- | -------------------------------------------- | | `--immutable` | `false` | Generate `readonly` properties and arrays | | `--alphabetize` | `false` | Sort generated types alphabetically | | `--export-type` | `false` | Export `type` instead of `interface` | | `--enum` | `false` | Generate TypeScript enums (vs string unions) | | `--default-non-nullable` | `true` | Properties with defaults are non-nullable | | `--path-params-as-types` | `false` | Enable dynamic string lookups on paths | | `--exclude-deprecated` | `false` | Omit deprecated fields | | `--check` | `false` | Verify types match current schema (CI) | --- ## Pattern 2: Multi-Schema Config with redocly.yaml For projects with multiple OpenAPI specs, use `redocly.yaml` (globbing is deprecated in v7). ```yaml # redocly.yaml apis: jobs@v1: root: ./specs/jobs.yaml x-openapi-ts: output: ./generated/jobs.ts companies@v1: root: ./specs/companies.yaml x-openapi-ts: output: ./generated/companies.ts # Per-schema overrides alphabetize: true immutable: true ``` ```bash # Generate all schemas (reads redocly.yaml automatically) npx openapi-typescript ``` ### Authenticated Remote Specs ```yaml # redocly.yaml resolve: http: headers: - matches: https://api.example.com/** name: X-API-KEY envVariable: API_KEY ``` --- ## Pattern 3: Using Generated Types The generated file exports `paths`, `components`, `operations`, and other top-level types. ```typescript import type { paths, components } from "./api/schema.d.ts"; // Access schema types directly type Job = components["schemas"]["Job"]; type Error = components["schemas"]["Error"]; type CreateJobInput = components["schemas"]["CreateJobInput"]; // Access path operation types type ListJobsQuery = paths["/jobs"]["get"]["parameters"]["query"]; type ListJobsResponse = paths["/jobs"]["get"]["responses"]["200"]["content"]["application/json"]; type GetJobResponse = paths["/jobs/{jobId}"]["get"]["responses"]["200"]["content"]["application/json"]; ``` **Why good:** all types derive from the spec, changing the spec and regenerating keeps everything in sync, zero runtime cost --- ## Pattern 4: openapi-fetch Client Setup `openapi-fetch` is a 6kb type-safe fetch wrapper. It infers all types from the generated `paths` type. ### Basic Client ```typescript import createClient from "openapi-fetch"; import type { paths } from "./api/schema.d.ts"; const API_BASE_URL = "https://api.example.com/v1"; export const apiClient = createClient<paths>({ baseUrl: API_BASE_URL, }); ``` ### CRUD Operations ```typescript // GET -- list with query params const { data: jobList, error: listError } = await apiClient.GET("/jobs", { params: { query: { page: 1, limit: 20, status: "active" }, }, }); // GET -- single resource with path param const { data: job, error: getError } = await apiClient.GET("/jobs/{jobId}", { params: { path: { jobId: "abc-123" } }, }); // POST -- create with request body const { data: newJob, error: createError } = await apiClient.POST("/jobs", { body: { title: "Senior Engineer", companyId: "company-456", salary: { min: 120000, max: 180000, currency: "USD" }, }, }); // PUT -- update const { data: updated, error: updateError } = await apiClient.PUT( "/jobs/{jobId}", { params: { path: { jobId: "abc-123" } }, body: { title: "Staff Engineer" }, }, ); // DELETE const { error: deleteError } = await apiClient.DELETE("/jobs/{jobId}", { params: { path: { jobId: "abc-123" } }, }); ``` **Why good:** paths, parameters, request bodies, and responses are all type-checked against the spec. Typos in paths or invalid params are compile-time errors. --- ## Pattern 5: openapi-fetch Middleware Middleware intercepts requests and responses. Use for auth headers, logging, and error handling. ### Auth Middleware ```typescript import type { Middleware } from "openapi-fetch"; let accessToken: string | undefined; const authMiddleware: Middleware = { async onRequest({ request }) { if (accessToken) { request.headers.set("Authorization", `Bearer ${accessToken}`); } return request; }, }; // Register middleware apiClient.use(authMiddleware); // Remove middleware later if needed apiClient.eject(authMiddleware); ``` ### Error Logging Middleware ```typescript const loggingMiddleware: Middleware = { async onResponse({ request, response }) { if (!response.ok) { console.error( `API error: ${request.method} ${request.url} -> ${response.status}`, ); } return response; }, }; apiClient.use(loggingMiddleware); ``` ### Error Throwing Middleware ```typescript const throwOnError: Middleware = { async onResponse({ response }) { if (!response.ok) { throw new Error(`${response.url}: ${response.status}`); } return response; }, }; ``` **Execution order:** `onRequest` callbacks run in registration order. `onResponse` callbacks run in reverse order. **Gotcha:** `onError` does NOT catch 4xx/5xx responses -- those are successful HTTP responses. Check `response.ok` or `response.status` in `onResponse` instead. `onError` only fires for network/fetch failures. --- ## Pattern 6: Response Handling Patterns ### Discriminated Error Handling ```typescript const { data, error, response } = await apiClient.GET("/jobs/{jobId}", { params: { path: { jobId } }, }); // data and error are discriminated -- only one is defined if (error) { // error is typed to the spec's error response schema switch (error.code) { case "not_found": // Handle 404 break; case "unauthorized": // Handle 401 break; default: // Unknown error break; } return; } // data is typed to the spec's 200 response schema console.log(data.title); ``` ### Accessing Response Headers ```typescript const { data, response } = await apiClient.GET("/jobs", { params: { query: { page: 1 } }, }); // response is the raw Response object const rateLimit = response.headers.get("X-RateLimit-Remaining"); const retryAfter = response.headers.get("Retry-After"); ``` --- ## Pattern 7: Programmatic Type Generation Use the Node.js API when you need custom transforms (e.g., converting `date-time` strings to `Date` types). ```typescript import fs from "node:fs"; import openapiTS, { astToString } from "openapi-typescript"; import ts from "typescript"; const DATE_TYPE = ts.factory.createTypeReferenceNode("Date"); const NULL_TYPE = ts.factory.createLiteralTypeNode(ts.factory.createNull()); const ast = await openapiTS(new URL("./api/openapi.yaml", import.meta.url), { transform(schemaObject) { // Convert date-time strings to Date type if (schemaObject.format === "date-time") { return Array.isArray(schemaObject.type) && schemaObject.type.includes("null") ? ts.factory.createUnionTypeNode([DATE_TYPE, NULL_TYPE]) : DATE_TYPE; } }, }); const contents = astToString(ast); fs.writeFileSync("./api/schema.ts", contents); ``` **When to use programmatic API:** custom type transforms (Date, Blob), build pipeline integration, dynamic schema loading, adding validation annotations to generated types. -
core.md 14.8 KB
# OpenAPI Core Patterns > Related: [codegen.md](codegen.md) for TypeScript generation, [validation.md](validation.md) for request/response validation --- ## Pattern 1: Complete Spec Structure A production-ready OpenAPI 3.1 document with all major sections. ```yaml openapi: "3.1.0" info: title: Jobs API version: "1.0.0" description: | REST API for job listings, applications, and company profiles. contact: name: API Support email: api@example.com license: name: MIT identifier: MIT servers: - url: https://api.example.com/v1 description: Production - url: https://staging-api.example.com/v1 description: Staging tags: - name: Jobs description: Job listing operations - name: Companies description: Company profile operations paths: /jobs: get: operationId: listJobs tags: [Jobs] summary: List job postings description: Returns a paginated list of active job postings. parameters: - $ref: "#/components/parameters/PageParam" - $ref: "#/components/parameters/LimitParam" - name: country in: query schema: type: string minLength: 2 maxLength: 2 description: ISO 3166-1 alpha-2 country code - name: status in: query schema: type: string enum: [active, closed, draft] default: active responses: "200": description: Paginated list of jobs content: application/json: schema: $ref: "#/components/schemas/JobListResponse" "400": $ref: "#/components/responses/BadRequest" post: operationId: createJob tags: [Jobs] summary: Create a job posting requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateJobInput" responses: "201": description: Job created content: application/json: schema: $ref: "#/components/schemas/Job" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" security: - bearerAuth: [] /jobs/{jobId}: get: operationId: getJob tags: [Jobs] summary: Get job details parameters: - $ref: "#/components/parameters/JobIdParam" responses: "200": description: Job details content: application/json: schema: $ref: "#/components/schemas/Job" "404": $ref: "#/components/responses/NotFound" put: operationId: updateJob tags: [Jobs] summary: Update a job posting parameters: - $ref: "#/components/parameters/JobIdParam" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateJobInput" responses: "200": description: Job updated content: application/json: schema: $ref: "#/components/schemas/Job" "404": $ref: "#/components/responses/NotFound" security: - bearerAuth: [] delete: operationId: deleteJob tags: [Jobs] summary: Delete a job posting parameters: - $ref: "#/components/parameters/JobIdParam" responses: "204": description: Job deleted "404": $ref: "#/components/responses/NotFound" security: - bearerAuth: [] components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT parameters: JobIdParam: name: jobId in: path required: true schema: type: string format: uuid PageParam: name: page in: query schema: type: integer minimum: 1 default: 1 LimitParam: name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 20 responses: BadRequest: description: Validation error content: application/json: schema: $ref: "#/components/schemas/Error" Unauthorized: description: Authentication required content: application/json: schema: $ref: "#/components/schemas/Error" NotFound: description: Resource not found content: application/json: schema: $ref: "#/components/schemas/Error" schemas: # --- Base schemas --- Pagination: type: object required: [page, limit, total, totalPages] properties: page: type: integer limit: type: integer total: type: integer totalPages: type: integer Error: type: object required: [code, message] properties: code: type: string description: Machine-readable error code examples: ["validation_error", "not_found"] message: type: string description: Human-readable error description details: type: array items: type: object properties: field: type: string message: type: string # --- Domain schemas --- Job: type: object required: [id, title, companyId, status, createdAt] properties: id: type: string format: uuid title: type: string minLength: 1 maxLength: 200 description: type: ["string", "null"] companyId: type: string format: uuid salary: $ref: "#/components/schemas/Salary" status: type: string enum: [active, closed, draft] tags: type: array items: type: string default: [] createdAt: type: string format: date-time updatedAt: type: ["string", "null"] format: date-time Salary: type: object required: [min, max, currency] properties: min: type: integer minimum: 0 max: type: integer minimum: 0 currency: type: string minLength: 3 maxLength: 3 description: ISO 4217 currency code CreateJobInput: type: object required: [title, companyId] properties: title: type: string minLength: 1 maxLength: 200 description: type: ["string", "null"] companyId: type: string format: uuid salary: $ref: "#/components/schemas/Salary" tags: type: array items: type: string UpdateJobInput: type: object properties: title: type: string minLength: 1 maxLength: 200 description: type: ["string", "null"] salary: $ref: "#/components/schemas/Salary" status: type: string enum: [active, closed, draft] tags: type: array items: type: string JobListResponse: type: object required: [data, pagination] properties: data: type: array items: $ref: "#/components/schemas/Job" pagination: $ref: "#/components/schemas/Pagination" ``` --- ## Pattern 2: Schema Composition with $ref ### allOf -- Extending a Base Schema Use `allOf` when a schema includes everything from a base plus additional fields. ```yaml # Base entity with audit fields BaseEntity: type: object required: [id, createdAt, updatedAt] properties: id: type: string format: uuid createdAt: type: string format: date-time updatedAt: type: string format: date-time # Job extends BaseEntity Job: allOf: - $ref: "#/components/schemas/BaseEntity" - type: object required: [title, status] properties: title: type: string status: type: string enum: [active, closed, draft] ``` **Why good:** base fields defined once, changes propagate to all extending schemas, generated types reflect inheritance ### oneOf with Discriminator -- Polymorphism Use `oneOf` with `discriminator` when the same field can hold different shapes identified by a type field. ```yaml Notification: oneOf: - $ref: "#/components/schemas/EmailNotification" - $ref: "#/components/schemas/SmsNotification" - $ref: "#/components/schemas/PushNotification" discriminator: propertyName: type mapping: email: "#/components/schemas/EmailNotification" sms: "#/components/schemas/SmsNotification" push: "#/components/schemas/PushNotification" EmailNotification: type: object required: [type, email, subject] properties: type: type: string const: email email: type: string format: email subject: type: string SmsNotification: type: object required: [type, phone, message] properties: type: type: string const: sms phone: type: string message: type: string PushNotification: type: object required: [type, deviceId, title] properties: type: type: string const: push deviceId: type: string title: type: string ``` **Why good:** explicit mapping aids code generators, `const` on discriminator field enables TypeScript narrowing, each variant is independently referenceable **Gotcha:** `discriminator` only works with `$ref` entries in `oneOf` -- inline schemas are ignored by the discriminator. --- ### anyOf -- Flexible Matching Use `anyOf` when a value can match one or more schemas simultaneously. ```yaml # Address can be domestic, international, or both (e.g., border zones) Address: anyOf: - $ref: "#/components/schemas/DomesticAddress" - $ref: "#/components/schemas/InternationalAddress" ``` **When to use `oneOf` vs `anyOf`:** Use `oneOf` when exactly one schema must match (mutually exclusive types). Use `anyOf` when multiple schemas could match simultaneously. --- ## Pattern 3: Reusable Parameters and Responses Extract repeated parameters and error responses into `components`. ```yaml components: parameters: # Reusable pagination params PageParam: name: page in: query schema: type: integer minimum: 1 default: 1 LimitParam: name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 20 # Reusable sort param SortParam: name: sort in: query schema: type: string enum: [createdAt, updatedAt, title] default: createdAt SortOrderParam: name: order in: query schema: type: string enum: [asc, desc] default: desc responses: BadRequest: description: Validation error content: application/json: schema: $ref: "#/components/schemas/Error" Unauthorized: description: Authentication required content: application/json: schema: $ref: "#/components/schemas/Error" Forbidden: description: Insufficient permissions content: application/json: schema: $ref: "#/components/schemas/Error" NotFound: description: Resource not found content: application/json: schema: $ref: "#/components/schemas/Error" TooManyRequests: description: Rate limit exceeded headers: Retry-After: schema: type: integer content: application/json: schema: $ref: "#/components/schemas/Error" ``` **Usage in paths:** ```yaml paths: /jobs: get: parameters: - $ref: "#/components/parameters/PageParam" - $ref: "#/components/parameters/LimitParam" - $ref: "#/components/parameters/SortParam" - $ref: "#/components/parameters/SortOrderParam" responses: "400": $ref: "#/components/responses/BadRequest" "429": $ref: "#/components/responses/TooManyRequests" ``` **Why good:** error shapes are consistent across all endpoints, pagination params defined once, adding a new standard response is a single change --- ## Pattern 4: OpenAPI 3.1 vs 3.0 Syntax Differences Key changes to be aware of when writing 3.1 specs. ### Nullable Fields ```yaml # 3.1 (correct) -- type array with "null" description: type: ["string", "null"] # 3.0 (outdated) -- nullable keyword removed in 3.1 # description: # type: string # nullable: true ``` ### Exclusive Min/Max ```yaml # 3.1 -- exclusiveMinimum is a number age: type: integer exclusiveMinimum: 0 # Must be > 0 # 3.0 (outdated) -- exclusiveMinimum was a boolean # age: # type: integer # minimum: 0 # exclusiveMinimum: true ``` ### $ref with Siblings ```yaml # 3.1 -- $ref can have sibling keywords (description overrides) salary: $ref: "#/components/schemas/Salary" description: Override the referenced schema's description # 3.0 -- siblings next to $ref were ignored ``` ### const Keyword ```yaml # 3.1 -- const for fixed values (from JSON Schema) type: type: string const: email # 3.0 -- had to use single-value enum # type: # type: string # enum: [email] ``` ### examples Keyword ```yaml # 3.1 -- examples as array (JSON Schema standard) email: type: string format: email examples: ["user@example.com", "admin@example.com"] # 3.0 -- single example keyword # email: # type: string # format: email # example: "user@example.com" ``` --- ## Pattern 5: Security Schemes Define authentication methods in `components/securitySchemes` and apply globally or per-operation. ```yaml components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT apiKeyAuth: type: apiKey in: header name: X-API-Key oauth2: type: oauth2 flows: authorizationCode: authorizationUrl: https://auth.example.com/authorize tokenUrl: https://auth.example.com/token scopes: read:jobs: Read job listings write:jobs: Create and update jobs # Apply globally (all operations require auth) security: - bearerAuth: [] # Override per-operation (public endpoint) paths: /jobs: get: operationId: listJobs security: [] # No auth required ``` **Why good:** security defined once, applied consistently, per-operation overrides for public endpoints -
validation.md 7.5 KB
# Request/Response Validation > Related: [core.md](core.md) for spec structure, [codegen.md](codegen.md) for type generation --- ## Pattern 1: Spec Validation and Linting Validate your OpenAPI spec for correctness before generating types or publishing documentation. Catch structural errors, missing `$ref` targets, and convention violations early. ### Redocly CLI ```bash # Install npm install -D @redocly/cli # Lint a spec npx redocly lint ./api/openapi.yaml # Bundle multi-file specs into a single file npx redocly bundle ./api/openapi.yaml -o ./api/bundled.yaml # Preview docs locally npx redocly preview-docs ./api/openapi.yaml ``` ### redocly.yaml Configuration ```yaml # redocly.yaml extends: - recommended rules: # Require operationId on every operation operation-operationId: error # Require descriptions on operations operation-description: warn # Require tags on operations operation-tag-defined: error # Reject unused components no-unused-components: warn # Require info contact info-contact: warn ``` **Why good:** catches missing `operationId`, broken `$ref`, unused schemas, and convention violations before they reach codegen --- ## Pattern 2: Runtime Validation Strategies ### Spec-Derived Validation (Recommended) When you write the spec first, derive runtime validation from it. The spec is the single source of truth for both types AND validation rules. **Strategy 1: Generate Zod schemas from spec** ```bash # Generate Zod schemas from OpenAPI spec npx @hey-api/openapi-ts \ -i ./api/openapi.yaml \ -o ./api/generated \ -p @hey-api/zod ``` This generates Zod schemas matching your OpenAPI schemas. Your validation layer uses generated schemas, keeping validation in sync with the spec automatically. **Strategy 2: Framework-native validation from spec** Many API frameworks read the OpenAPI spec directly and validate incoming requests against it at the middleware level. The spec defines the validation rules, the framework enforces them -- no separate validation code to maintain. ### Code-Derived Spec (Alternative) When you write validation schemas first (code-first), generate the spec from them. Libraries like `zod-to-openapi` or framework-specific OpenAPI integrations derive the OpenAPI spec from your Zod schemas. ```typescript // Code-first: Zod schema IS the source of truth // OpenAPI spec is generated from it // (import z from your framework's OpenAPI integration) import { z } from "zod-to-openapi"; const CreateJobSchema = z .object({ title: z.string().min(1).max(200), companyId: z.string().uuid(), }) .openapi("CreateJobInput"); ``` **When to use code-first:** Framework provides first-class OpenAPI generation, rapid prototyping, single-team internal APIs. --- ## Pattern 3: Request Validation Patterns Regardless of spec-first or code-first, these patterns apply to validating incoming requests. ### Path Parameter Validation ```yaml # In the spec parameters: - name: jobId in: path required: true schema: type: string format: uuid ``` The `format: uuid` constraint should be enforced at runtime. Either your framework validates against the spec, or your validation layer checks the format. ### Query Parameter Validation with Defaults ```yaml parameters: - name: page in: query schema: type: integer minimum: 1 default: 1 - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 20 - name: sort in: query schema: type: string enum: [createdAt, updatedAt, title] default: createdAt ``` **Key point:** Query params arrive as strings over HTTP. Your validation layer must coerce `"20"` to `20` for integer params. ### Request Body Validation ```yaml requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateJobInput" ``` The `required: true` means the body must be present. The schema's own `required` array defines which fields within the body are mandatory. --- ## Pattern 4: Response Validation Validate outgoing responses in development/testing to catch implementation drift from the spec. ### Development-Time Response Validation ```typescript // Only in development -- validate responses match spec function validateResponse( path: string, method: string, statusCode: number, body: unknown, ): void { if (process.env.NODE_ENV !== "development") return; // Use your spec validation library to check body against // the response schema for this path/method/status const isValid = specValidator.validateResponse( path, method, statusCode, body, ); if (!isValid) { console.warn( `Response for ${method.toUpperCase()} ${path} (${statusCode}) does not match spec`, specValidator.errors, ); } } ``` **Why validate responses:** Catches implementation bugs where the handler returns data that doesn't match the spec. Run in development/CI, not production. --- ## Pattern 5: Error Response Contract Define a consistent error shape in the spec and enforce it across all endpoints. ```yaml components: schemas: Error: type: object required: [code, message] properties: code: type: string description: Machine-readable error code examples: ["validation_error", "not_found", "unauthorized"] message: type: string description: Human-readable message details: type: array items: type: object required: [field, message] properties: field: type: string message: type: string ``` ### Consistent Error Formatting ```typescript // Named error codes from the spec const ERROR_CODES = { VALIDATION_ERROR: "validation_error", NOT_FOUND: "not_found", UNAUTHORIZED: "unauthorized", FORBIDDEN: "forbidden", INTERNAL_ERROR: "internal_error", } as const; interface ApiError { code: string; message: string; details?: Array<{ field: string; message: string }>; } function formatValidationError( issues: Array<{ path: string; message: string }>, ): ApiError { return { code: ERROR_CODES.VALIDATION_ERROR, message: "Validation failed", details: issues.map((issue) => ({ field: issue.path, message: issue.message, })), }; } function formatNotFoundError(resource: string, id: string): ApiError { return { code: ERROR_CODES.NOT_FOUND, message: `${resource} with id '${id}' not found`, }; } ``` **Why good:** every endpoint returns the same error shape, clients can reliably parse errors, machine-readable codes enable programmatic handling --- ## Pattern 6: CI Pipeline Integration Validate specs and check type freshness in CI to prevent drift. ```bash # 1. Lint the spec npx redocly lint ./api/openapi.yaml # 2. Check if generated types are up to date npx openapi-typescript ./api/openapi.yaml --check # 3. Type-check the project (catches mismatches between generated types and code) npx tsc --noEmit ``` **`--check` flag:** Compares the current generated file against what the spec would produce. Fails if they differ -- forces developers to regenerate types after spec changes. ### package.json Scripts ```json { "scripts": { "api:lint": "redocly lint ./api/openapi.yaml", "api:generate": "openapi-typescript ./api/openapi.yaml -o ./api/schema.d.ts", "api:check": "openapi-typescript ./api/openapi.yaml --check", "precommit": "npm run api:lint && npm run api:check && tsc --noEmit" } } ```
-
-
reference.md 6.3 KB
# OpenAPI Quick Reference ## OpenAPI 3.0 to 3.1 Migration | Feature | 3.0 Syntax | 3.1 Syntax | | ----------------- | ---------------------------------- | -------------------------------------- | | Nullable | `nullable: true` | `type: ["string", "null"]` | | Exclusive min/max | `exclusiveMinimum: true` (boolean) | `exclusiveMinimum: 0` (number) | | `$ref` siblings | Siblings ignored | Siblings allowed (e.g., `description`) | | Constant value | `enum: [value]` | `const: value` | | Examples | `example: "val"` | `examples: ["val1", "val2"]` | | JSON Schema | Subset (extended) | Superset of Draft 2020-12 | --- ## Schema Type Quick Reference | OpenAPI Type | Format | TypeScript | Notes | | -------------------- | ----------- | ------------------- | -------------------------- | | `string` | — | `string` | | | `string` | `email` | `string` | Validated format | | `string` | `uri` | `string` | Validated format | | `string` | `uuid` | `string` | Validated format | | `string` | `date` | `string` | ISO 8601 date | | `string` | `date-time` | `string` | ISO 8601 datetime | | `string` | `password` | `string` | UI hint only | | `string` | `binary` | `Blob` | File upload | | `integer` | — | `number` | | | `integer` | `int32` | `number` | 32-bit | | `integer` | `int64` | `number` | 64-bit (JS precision loss) | | `number` | — | `number` | | | `number` | `float` | `number` | | | `number` | `double` | `number` | | | `boolean` | — | `boolean` | | | `array` | — | `T[]` | `items` required | | `object` | — | `Record<string, T>` | Or typed properties | | `["string", "null"]` | — | `string \| null` | 3.1 nullable | --- ## Composition Cheat Sheet | Keyword | Meaning | Use Case | | --------------- | ------------------------ | --------------------------------- | | `$ref` | Reference another schema | Reuse, DRY | | `allOf` | Must match ALL schemas | Extend base schema | | `oneOf` | Must match exactly ONE | Polymorphism with discriminator | | `anyOf` | Must match ONE or MORE | Flexible matching | | `not` | Must NOT match | Exclusion constraint | | `discriminator` | Hint for codegen | Identifies variant by field value | --- ## openapi-typescript CLI Flags | Flag | Short | Default | Purpose | | ------------------------- | ----- | ------- | ------------------------- | | `--output` | `-o` | stdout | Output file path | | `--immutable` | — | `false` | `readonly` properties | | `--alphabetize` | — | `false` | Sort types | | `--export-type` | `-t` | `false` | `type` vs `interface` | | `--enum` | — | `false` | TS enums vs unions | | `--default-non-nullable` | — | `true` | Defaults are non-nullable | | `--path-params-as-types` | — | `false` | Dynamic path lookups | | `--exclude-deprecated` | — | `false` | Omit deprecated fields | | `--check` | — | `false` | CI freshness check | | `--additional-properties` | — | `false` | Allow extra properties | | `--array-length` | — | `false` | Tuple from min/maxItems | --- ## openapi-fetch API Reference ```typescript import createClient from "openapi-fetch"; import type { paths } from "./schema.d.ts"; // Create client const client = createClient<paths>({ baseUrl: "https://api.example.com/v1" }); // HTTP methods const { data, error, response } = await client.GET("/path/{id}", { params: { path: { id: "123" }, query: { page: 1 } }, }); const { data } = await client.POST("/path", { body: { field: "value" }, }); const { data } = await client.PUT("/path/{id}", { params: { path: { id: "123" } }, body: { field: "updated" }, }); const { error } = await client.DELETE("/path/{id}", { params: { path: { id: "123" } }, }); // Middleware client.use(middleware); // Register client.eject(middleware); // Remove ``` ### Middleware Interface ```typescript import type { Middleware } from "openapi-fetch"; const middleware: Middleware = { async onRequest({ request, options }) { // Modify request before sending return request; // or undefined to skip }, async onResponse({ request, response, options }) { // Inspect/modify response return response; }, async onError({ error }) { // Handle fetch failures (NOT 4xx/5xx) return new Error("Network error", { cause: error }); }, }; ``` --- ## Decision Framework ### When to Use Which Tool ``` Need TypeScript types from a spec? +-- openapi-typescript (zero-runtime .d.ts files) Need a type-safe HTTP client? +-- openapi-fetch (6kb, uses generated paths type) Need full SDK with Zod schemas / query hooks? +-- @hey-api/openapi-ts (plugin ecosystem) Need to lint / validate a spec? +-- Redocly CLI (rules, bundling, preview) Need to generate the spec from code? +-- Use your framework's OpenAPI integration ``` ### Spec Organization ``` Single spec under 500 lines? +-- Single YAML file Spec growing beyond 500 lines? +-- Split by domain into multiple files, use $ref across files Multiple independent APIs? +-- Separate specs, redocly.yaml for multi-schema generation ``` -
SKILL.md 13.3 KB
--- name: api-specs-openapi description: OpenAPI 3.1 specification, schema design, code generation --- # OpenAPI Specification Patterns > **Quick Guide:** Use OpenAPI 3.1 for API contracts. 3.1 is a superset of JSON Schema Draft 2020-12 -- use `type: ["string", "null"]` instead of `nullable: true`. Define all reusable schemas in `components/schemas` and reference with `$ref`. Always include `operationId` on every operation (it becomes the client method name). Use `openapi-typescript` to generate zero-runtime TypeScript types and `openapi-fetch` for a 6kb type-safe fetch client. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use OpenAPI 3.1 syntax -- `type: ["string", "null"]` NOT the 3.0 `nullable: true` keyword)** **(You MUST define reusable schemas in `components/schemas` and reference with `$ref` -- NO inline schema duplication)** **(You MUST include `operationId` on every path operation -- it becomes the generated client method name)** **(You MUST use `openapi-typescript` for type generation and import types with `import type` -- types are zero-runtime)** </critical_requirements> --- **Auto-detection:** OpenAPI, openapi, swagger, openapi-typescript, openapi-fetch, createClient, paths, components, schemas, operationId, $ref, discriminator, oneOf, allOf, anyOf, openapi: "3.1", spec-first, API contract, API specification, code generation, schema design **When to use:** - Defining API contracts before or alongside implementation (spec-first or code-first) - Generating TypeScript types from an existing OpenAPI spec - Building type-safe API clients with automatic request/response validation - Documenting REST APIs for external or internal consumers - Designing reusable schema components with `$ref` composition **When NOT to use:** - Internal-only endpoints with no external consumers and no documentation needs - GraphQL APIs (use GraphQL schema tooling instead) - Simple scripts or prototypes where formal contracts add overhead **Key patterns covered:** - OpenAPI 3.1 spec structure (info, paths, components, servers) - Schema design with JSON Schema Draft 2020-12 alignment - `$ref` composition, `oneOf`/`allOf`/`anyOf`, discriminators - Path operations with parameters, request bodies, and responses - TypeScript type generation with `openapi-typescript` v7 - Type-safe fetch client with `openapi-fetch` - Spec-first vs code-first decision framework **Detailed Resources:** - [examples/core.md](examples/core.md) - Spec structure, schemas, paths, operations, `$ref` composition - [examples/codegen.md](examples/codegen.md) - TypeScript type generation, `openapi-fetch` client - [examples/validation.md](examples/validation.md) - Request/response validation, middleware patterns - [reference.md](reference.md) - Decision frameworks, anti-patterns, quick-lookup tables --- <philosophy> ## Philosophy **The spec IS the contract.** An OpenAPI document is the single source of truth for your API's shape. Types, documentation, client SDKs, and server validation are all derived from it -- never maintained separately. **OpenAPI 3.1 aligns with JSON Schema Draft 2020-12.** This means any valid JSON Schema is a valid OpenAPI schema. Use `type` arrays for nullable (`["string", "null"]`), `if/then/else` for conditional schemas, and standard JSON Schema vocabulary. **Spec-first (design-first) is recommended** for stable, multi-consumer APIs. Define the contract first, get feedback from consumers via mocks, then implement. Code-first works for rapid prototypes where the spec is generated from annotations. **Use spec-first when:** - Multiple teams consume the API - API is public or has external consumers - Contract stability matters (breaking changes are expensive) - You want mocks and docs before writing any code **Use code-first when:** - Rapid prototyping where the spec is generated from code annotations - Single-team internal APIs where the implementation IS the contract - Framework provides first-class OpenAPI generation from code annotations </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Spec Structure Every OpenAPI 3.1 document has four required top-level fields: `openapi`, `info`, `paths` (or `webhooks`), and implicitly `components` for reusable schemas. ```yaml openapi: "3.1.0" info: title: Jobs API version: "1.0.0" description: Job listings and applications servers: - url: https://api.example.com/v1 paths: /jobs: get: operationId: listJobs # ... components: schemas: Job: # ... ``` **Why good:** `operationId` becomes the client method name, `servers` enables environment switching, schemas in `components` are reusable via `$ref` See [examples/core.md](examples/core.md) for complete spec with paths, parameters, and responses. --- ### Pattern 2: Schema Design with 3.1 Syntax OpenAPI 3.1 uses JSON Schema Draft 2020-12. Key differences from 3.0: `nullable` is removed, use `type` arrays instead. `exclusiveMinimum`/`exclusiveMaximum` are numbers, not booleans. ```yaml # 3.1 nullable syntax type: ["string", "null"] # NOT 3.0 syntax: # type: string # nullable: true ``` ```yaml components: schemas: Salary: type: object required: [min, max, currency] properties: min: type: integer minimum: 0 max: type: integer minimum: 0 currency: type: string minLength: 3 maxLength: 3 description: ISO 4217 currency code ``` **Why good:** aligns with standard JSON Schema, tooling ecosystem understands it natively, `required` array is explicit See [examples/core.md](examples/core.md) for enum, format, and composition examples. --- ### Pattern 3: $ref Composition and Reuse Define schemas once in `components/schemas`, reference everywhere with `$ref`. Use `allOf` to extend base schemas, `oneOf` for polymorphism with discriminators. ```yaml components: schemas: PaginatedResponse: type: object required: [data, pagination] properties: pagination: $ref: "#/components/schemas/Pagination" JobListResponse: allOf: - $ref: "#/components/schemas/PaginatedResponse" - type: object properties: data: type: array items: $ref: "#/components/schemas/Job" ``` **Why good:** single source of truth, changes propagate automatically, generated types reflect composition See [examples/core.md](examples/core.md) for `oneOf` with discriminator and `allOf` extension patterns. --- ### Pattern 4: Path Operations Operations define HTTP methods on paths. Always include `operationId`, `tags`, parameter schemas, and all response codes. ```yaml paths: /jobs/{jobId}: get: operationId: getJob tags: [Jobs] parameters: - name: jobId in: path required: true schema: type: string format: uuid responses: "200": description: Job details content: application/json: schema: $ref: "#/components/schemas/Job" "404": description: Job not found content: application/json: schema: $ref: "#/components/schemas/Error" ``` **Why good:** `operationId` drives codegen method names, explicit `404` response documents error cases, `format: uuid` aids validation See [examples/core.md](examples/core.md) for query parameters, request bodies, and pagination. --- ### Pattern 5: TypeScript Type Generation Use `openapi-typescript` v7 to generate zero-runtime types from your spec. Types are generated as `.d.ts` files and imported with `import type`. ```bash npx openapi-typescript ./api/openapi.yaml -o ./api/schema.d.ts ``` ```typescript import type { paths, components } from "./api/schema.d.ts"; // Access schema types type Job = components["schemas"]["Job"]; type Error = components["schemas"]["Error"]; // Access response types type JobListResponse = paths["/jobs"]["get"]["responses"]["200"]["content"]["application/json"]; ``` **Why good:** zero runtime cost, types stay in sync with spec, no manual interface maintenance See [examples/codegen.md](examples/codegen.md) for CLI options, `redocly.yaml` multi-schema config, and programmatic API. --- ### Pattern 6: Type-Safe Fetch Client Use `openapi-fetch` (6kb) for a type-safe client that infers request/response types from generated types. No codegen needed beyond the types. ```typescript import createClient from "openapi-fetch"; import type { paths } from "./api/schema.d.ts"; const client = createClient<paths>({ baseUrl: "https://api.example.com/v1" }); // Fully typed -- path params, query, body, response const { data, error } = await client.GET("/jobs/{jobId}", { params: { path: { jobId: "abc-123" } }, }); if (error) { // error is typed to the spec's error response schema console.error(error); return; } // data is typed to the spec's 200 response schema console.log(data.title); ``` **Why good:** 6kb with virtually zero runtime, no manual generics, path/query/body/response all type-checked against the spec See [examples/codegen.md](examples/codegen.md) for middleware, auth headers, and error handling. </patterns> --- <decision_framework> ## Decision Framework ### Spec-First vs Code-First ``` Is this a public or multi-consumer API? |-- YES --> Spec-first (design contract, get feedback, then implement) +-- NO --> Is this a rapid prototype? |-- YES --> Code-first (generate spec from annotations) +-- NO --> Does your framework generate OpenAPI from code? |-- YES --> Code-first (framework handles spec generation) +-- NO --> Spec-first (write the YAML, generate types) ``` ### Schema Composition ``` Need to share fields across schemas? |-- YES --> allOf with a base $ref schema +-- NO --> Need polymorphism (multiple possible shapes)? |-- YES --> oneOf with discriminator +-- NO --> Need to combine constraints? |-- YES --> allOf (all must match) +-- NO --> Simple schema with $ref for nested objects ``` ### Type Generation Tooling | Need | Tool | When | | -------------------------- | ----------------------- | ------------------------------------------- | | TypeScript types from spec | `openapi-typescript` v7 | Always -- zero-runtime type generation | | Type-safe fetch client | `openapi-fetch` | Frontend or service-to-service calls | | Full SDK generation | `@hey-api/openapi-ts` | Need Zod schemas, query hooks, or full SDKs | | Spec validation/linting | Redocly CLI | CI pipeline, pre-commit checks | </decision_framework> --- <red_flags> ## RED FLAGS **High Priority:** - Using `nullable: true` -- removed in OpenAPI 3.1, use `type: ["string", "null"]` - Duplicating schemas inline instead of using `$ref` to `components/schemas` -- creates drift - Missing `operationId` on operations -- generated clients get ugly auto-generated names - Manually maintaining TypeScript interfaces that mirror the spec -- use `openapi-typescript` to generate them - Using `openapi: "3.0.x"` when 3.1 is available -- misses JSON Schema alignment **Medium Priority:** - Defining error responses without a shared `Error` schema -- inconsistent error shapes across endpoints - Missing `required` array on object schemas -- all properties become optional by default - Using `type: object` without `additionalProperties: false` when extra fields should be rejected - Not documenting `4xx`/`5xx` responses -- consumers don't know what error shapes to expect **Gotchas & Edge Cases:** - `$ref` siblings are ignored in 3.0 but allowed in 3.1 -- `description` next to `$ref` now works in 3.1 - `exclusiveMinimum`/`exclusiveMaximum` changed from boolean (3.0) to number (3.1) -- `exclusiveMinimum: 0` means "greater than 0" - `discriminator` does not affect validation -- it's a hint for code generators, not a constraint - `discriminator.propertyName` must be a required string property at the same schema level - Inline schemas inside `discriminator` `oneOf` are not considered -- only `$ref` entries work - `openapi-typescript` generates `.d.ts` files -- these are type-only, no runtime code - `openapi-fetch` `data` is only present for 2xx responses, `error` for 4xx/5xx -- always check which is defined - Response bodies are consumed once -- clone the response if middleware needs to read it and pass it through - `openapi-typescript` v7 uses `redocly.yaml` for multi-schema config -- globbing is deprecated </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** **(You MUST use OpenAPI 3.1 syntax -- `type: ["string", "null"]` NOT the 3.0 `nullable: true` keyword)** **(You MUST define reusable schemas in `components/schemas` and reference with `$ref` -- NO inline schema duplication)** **(You MUST include `operationId` on every path operation -- it becomes the generated client method name)** **(You MUST use `openapi-typescript` for type generation and import types with `import type` -- types are zero-runtime)** **Failure to follow these rules will cause schema drift, broken codegen, and type mismatches between spec and implementation.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.