Claude Skill

api-specs-openapi

OpenAPI 3.1 specification, schema design, code generation

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-specs-openapi_skills_api-specs-openapi-3a51ef5.zip · 16 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-specs-openapi/skills/api-specs-openapi
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git 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 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:




<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>

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.

No comments yet.

Reviews (0)

No reviews yet.

Related