openapi-spec-generation · diff
git:20260307.47a5dbc to git:20260326.db33dbc
5 added, 486 removed. Audit A to A.
---
name: openapi-spec-generation
description: Generate and maintain OpenAPI 3.1 specifications from code, design-first specs, and validation patterns. Use when creating API documentation, generating SDKs, or ensuring API contract compliance.
---
# OpenAPI Spec Generation
Comprehensive patterns for creating, maintaining, and validating OpenAPI 3.1 specifications for RESTful APIs.
## When to Use This Skill
- Creating API documentation from scratch
- Generating OpenAPI specs from existing code
- Designing API contracts (design-first approach)
- Validating API implementations against specs
- Generating client SDKs from specs
- Setting up API documentation portals
## Core Concepts
### 1. OpenAPI 3.1 Structure
```yaml
openapi: 3.1.0
info:
title: API Title
version: 1.0.0
servers:
- url: https://api.example.com/v1
paths:
/resources:
get: ...
components:
schemas: ...
securitySchemes: ...
```
### 2. Design Approaches
| Approach | Description | Best For |
| ---------------- | ---------------------------- | ------------------- |
| **Design-First** | Write spec before code | New APIs, contracts |
| **Code-First** | Generate spec from code | Existing APIs |
| **Hybrid** | Annotate code, generate spec | Evolving APIs |
## Templates
### Template 1: Complete API Specification
```yaml
openapi: 3.1.0
info:
title: User Management API
description: |
API for managing users and their profiles.
## Authentication
All endpoints require Bearer token authentication.
## Rate Limiting
- 1000 requests per minute for standard tier
- 10000 requests per minute for enterprise tier
version: 2.0.0
contact:
name: API Support
email: api-support@example.com
url: https://docs.example.com
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: https://api.example.com/v2
description: Production
- url: https://staging-api.example.com/v2
description: Staging
- url: http://localhost:3000/v2
description: Local development
tags:
- name: Users
description: User management operations
- name: Profiles
description: User profile operations
- name: Admin
description: Administrative operations
paths:
/users:
get:
operationId: listUsers
summary: List all users
description: Returns a paginated list of users with optional filtering.
tags:
- Users
parameters:
- $ref: "#/components/parameters/PageParam"
- $ref: "#/components/parameters/LimitParam"
- name: status
in: query
description: Filter by user status
schema:
$ref: "#/components/schemas/UserStatus"
- name: search
in: query
description: Search by name or email
schema:
type: string
minLength: 2
maxLength: 100
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: "#/components/schemas/UserListResponse"
examples:
default:
$ref: "#/components/examples/UserListExample"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/RateLimited"
security:
- bearerAuth: []
post:
operationId: createUser
summary: Create a new user
description: Creates a new user account and sends welcome email.
tags:
- Users
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUserRequest"
examples:
standard:
summary: Standard user
value:
email: user@example.com
name: John Doe
role: user
admin:
summary: Admin user
value:
email: admin@example.com
name: Admin User
role: admin
responses:
"201":
description: User created successfully
content:
application/json:
schema:
$ref: "#/components/schemas/User"
headers:
Location:
description: URL of created user
schema:
type: string
format: uri
"400":
$ref: "#/components/responses/BadRequest"
"409":
description: Email already exists
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
security:
- bearerAuth: []
/users/{userId}:
parameters:
- $ref: "#/components/parameters/UserIdParam"
get:
operationId: getUser
summary: Get user by ID
tags:
- Users
responses:
"200":
description: Successful response
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"404":
$ref: "#/components/responses/NotFound"
security:
- bearerAuth: []
patch:
operationId: updateUser
summary: Update user
tags:
- Users
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateUserRequest"
responses:
"200":
description: User updated
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
security:
- bearerAuth: []
delete:
operationId: deleteUser
summary: Delete user
tags:
- Users
- Admin
responses:
"204":
description: User deleted
"404":
$ref: "#/components/responses/NotFound"
security:
- bearerAuth: []
- apiKey: []
components:
schemas:
User:
type: object
required:
- id
- email
- name
- status
- createdAt
properties:
id:
type: string
format: uuid
readOnly: true
description: Unique user identifier
email:
type: string
format: email
description: User email address
name:
type: string
minLength: 1
maxLength: 100
description: User display name
status:
$ref: "#/components/schemas/UserStatus"
role:
type: string
enum: [user, moderator, admin]
default: user
avatar:
type: string
format: uri
nullable: true
metadata:
type: object
additionalProperties: true
description: Custom metadata
createdAt:
type: string
format: date-time
readOnly: true
updatedAt:
type: string
format: date-time
readOnly: true
UserStatus:
type: string
enum: [active, inactive, suspended, pending]
description: User account status
CreateUserRequest:
type: object
required:
- email
- name
properties:
email:
type: string
format: email
name:
type: string
minLength: 1
maxLength: 100
role:
type: string
enum: [user, moderator, admin]
default: user
metadata:
type: object
additionalProperties: true
UpdateUserRequest:
type: object
minProperties: 1
properties:
name:
type: string
minLength: 1
maxLength: 100
status:
$ref: "#/components/schemas/UserStatus"
role:
type: string
enum: [user, moderator, admin]
metadata:
type: object
additionalProperties: true
UserListResponse:
type: object
required:
- data
- pagination
properties:
data:
type: array
items:
$ref: "#/components/schemas/User"
pagination:
$ref: "#/components/schemas/Pagination"
Pagination:
type: object
required:
- page
- limit
- total
- totalPages
properties:
page:
type: integer
minimum: 1
limit:
type: integer
minimum: 1
maximum: 100
total:
type: integer
minimum: 0
totalPages:
type: integer
minimum: 0
hasNext:
type: boolean
hasPrev:
type: boolean
Error:
type: object
required:
- code
- message
properties:
code:
type: string
description: Error code for programmatic handling
message:
type: string
description: Human-readable error message
details:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
requestId:
type: string
description: Request ID for support
parameters:
UserIdParam:
name: userId
in: path
required: true
description: User ID
schema:
type: string
format: uuid
PageParam:
name: page
in: query
description: Page number (1-based)
schema:
type: integer
minimum: 1
default: 1
LimitParam:
name: limit
in: query
description: Items per page
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
BadRequest:
description: Invalid request
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
code: VALIDATION_ERROR
message: Invalid request parameters
details:
- field: email
message: Must be a valid email address
Unauthorized:
description: Authentication required
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
code: UNAUTHORIZED
message: Authentication required
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
example:
code: NOT_FOUND
message: User not found
RateLimited:
description: Too many requests
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
headers:
Retry-After:
description: Seconds until rate limit resets
schema:
type: integer
X-RateLimit-Limit:
description: Request limit per window
schema:
type: integer
X-RateLimit-Remaining:
description: Remaining requests in window
schema:
type: integer
examples:
UserListExample:
value:
data:
- id: "550e8400-e29b-41d4-a716-446655440000"
email: "john@example.com"
name: "John Doe"
status: "active"
role: "user"
createdAt: "2024-01-15T10:30:00Z"
pagination:
page: 1
limit: 20
total: 1
totalPages: 1
hasNext: false
hasPrev: false
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: JWT token from /auth/login
apiKey:
type: apiKey
in: header
name: X-API-Key
description: API key for service-to-service calls
security:
- bearerAuth: []
```
- ### Template 2: Code-First Generation (Python/FastAPI)
-
- ```python
- # FastAPI with automatic OpenAPI generation
- from fastapi import FastAPI, HTTPException, Query, Path, Depends
- from pydantic import BaseModel, Field, EmailStr
- from typing import Optional, List
- from datetime import datetime
- from uuid import UUID
- from enum import Enum
-
- app = FastAPI(
- title="User Management API",
- description="API for managing users and profiles",
- version="2.0.0",
- openapi_tags=[
- {"name": "Users", "description": "User operations"},
- {"name": "Profiles", "description": "Profile operations"},
- ],
- servers=[
- {"url": "https://api.example.com/v2", "description": "Production"},
- {"url": "http://localhost:8000", "description": "Development"},
- ],
- )
-
- # Enums
- class UserStatus(str, Enum):
- active = "active"
- inactive = "inactive"
- suspended = "suspended"
- pending = "pending"
-
- class UserRole(str, Enum):
- user = "user"
- moderator = "moderator"
- admin = "admin"
-
- # Models
- class UserBase(BaseModel):
- email: EmailStr = Field(..., description="User email address")
- name: str = Field(..., min_length=1, max_length=100, description="Display name")
-
- class UserCreate(UserBase):
- role: UserRole = Field(default=UserRole.user)
- metadata: Optional[dict] = Field(default=None, description="Custom metadata")
-
- model_config = {
- "json_schema_extra": {
- "examples": [
- {
- "email": "user@example.com",
- "name": "John Doe",
- "role": "user"
- }
- ]
- }
- }
-
- class UserUpdate(BaseModel):
- name: Optional[str] = Field(None, min_length=1, max_length=100)
- status: Optional[UserStatus] = None
- role: Optional[UserRole] = None
- metadata: Optional[dict] = None
-
- class User(UserBase):
- id: UUID = Field(..., description="Unique identifier")
- status: UserStatus
- role: UserRole
- avatar: Optional[str] = Field(None, description="Avatar URL")
- metadata: Optional[dict] = None
- created_at: datetime = Field(..., alias="createdAt")
- updated_at: Optional[datetime] = Field(None, alias="updatedAt")
-
- model_config = {"populate_by_name": True}
-
- class Pagination(BaseModel):
- page: int = Field(..., ge=1)
- limit: int = Field(..., ge=1, le=100)
- total: int = Field(..., ge=0)
- total_pages: int = Field(..., ge=0, alias="totalPages")
- has_next: bool = Field(..., alias="hasNext")
- has_prev: bool = Field(..., alias="hasPrev")
-
- class UserListResponse(BaseModel):
- data: List[User]
- pagination: Pagination
-
- class ErrorDetail(BaseModel):
- field: str
- message: str
-
- class ErrorResponse(BaseModel):
- code: str = Field(..., description="Error code")
- message: str = Field(..., description="Error message")
- details: Optional[List[ErrorDetail]] = None
- request_id: Optional[str] = Field(None, alias="requestId")
-
- # Endpoints
- @app.get(
- "/users",
- response_model=UserListResponse,
- tags=["Users"],
- summary="List all users",
- description="Returns a paginated list of users with optional filtering.",
- responses={
- 400: {"model": ErrorResponse, "description": "Invalid request"},
- 401: {"model": ErrorResponse, "description": "Unauthorized"},
- },
- )
- async def list_users(
- page: int = Query(1, ge=1, description="Page number"),
- limit: int = Query(20, ge=1, le=100, description="Items per page"),
- status: Optional[UserStatus] = Query(None, description="Filter by status"),
- search: Optional[str] = Query(None, min_length=2, max_length=100),
- ):
- """
- List users with pagination and filtering.
-
- - **page**: Page number (1-based)
- - **limit**: Number of items per page (max 100)
- - **status**: Filter by user status
- - **search**: Search by name or email
- """
- # Implementation
- pass
-
- @app.post(
- "/users",
- response_model=User,
- status_code=201,
- tags=["Users"],
- summary="Create a new user",
- responses={
- 400: {"model": ErrorResponse},
- 409: {"model": ErrorResponse, "description": "Email already exists"},
- },
- )
- async def create_user(user: UserCreate):
- """Create a new user and send welcome email."""
- pass
-
- @app.get(
- "/users/{user_id}",
- response_model=User,
- tags=["Users"],
- summary="Get user by ID",
- responses={404: {"model": ErrorResponse}},
- )
- async def get_user(
- user_id: UUID = Path(..., description="User ID"),
- ):
- """Retrieve a specific user by their ID."""
- pass
-
- @app.patch(
- "/users/{user_id}",
- response_model=User,
- tags=["Users"],
- summary="Update user",
- responses={
- 400: {"model": ErrorResponse},
- 404: {"model": ErrorResponse},
- },
- )
- async def update_user(
- user_id: UUID = Path(..., description="User ID"),
- user: UserUpdate = ...,
- ):
- """Update user attributes."""
- pass
-
- @app.delete(
- "/users/{user_id}",
- status_code=204,
- tags=["Users", "Admin"],
- summary="Delete user",
- responses={404: {"model": ErrorResponse}},
- )
- async def delete_user(
- user_id: UUID = Path(..., description="User ID"),
- ):
- """Permanently delete a user."""
- pass
-
- # Export OpenAPI spec
- if __name__ == "__main__":
- import json
- print(json.dumps(app.openapi(), indent=2))
- ```
-
- ### Template 3: Code-First (TypeScript/Express with tsoa)
-
- ```typescript
- // tsoa generates OpenAPI from TypeScript decorators
-
- import {
- Controller,
- Get,
- Post,
- Patch,
- Delete,
- Route,
- Path,
- Query,
- Body,
- Response,
- SuccessResponse,
- Tags,
- Security,
- Example,
- } from "tsoa";
-
- // Models
- interface User {
- /** Unique identifier */
- id: string;
- /** User email address */
- email: string;
- /** Display name */
- name: string;
- status: UserStatus;
- role: UserRole;
- /** Avatar URL */
- avatar?: string;
- /** Custom metadata */
- metadata?: Record<string, unknown>;
- createdAt: Date;
- updatedAt?: Date;
- }
-
- enum UserStatus {
- Active = "active",
- Inactive = "inactive",
- Suspended = "suspended",
- Pending = "pending",
- }
-
- enum UserRole {
- User = "user",
- Moderator = "moderator",
- Admin = "admin",
- }
-
- interface CreateUserRequest {
- email: string;
- name: string;
- role?: UserRole;
- metadata?: Record<string, unknown>;
- }
-
- interface UpdateUserRequest {
- name?: string;
- status?: UserStatus;
- role?: UserRole;
- metadata?: Record<string, unknown>;
- }
-
- interface Pagination {
- page: number;
- limit: number;
- total: number;
- totalPages: number;
- hasNext: boolean;
- hasPrev: boolean;
- }
-
- interface UserListResponse {
- data: User[];
- pagination: Pagination;
- }
-
- interface ErrorResponse {
- code: string;
- message: string;
- details?: { field: string; message: string }[];
- requestId?: string;
- }
-
- @Route("users")
- @Tags("Users")
- export class UsersController extends Controller {
- /**
- * List all users with pagination and filtering
- * @param page Page number (1-based)
- * @param limit Items per page (max 100)
- * @param status Filter by user status
- * @param search Search by name or email
- */
- @Get()
- @Security("bearerAuth")
- @Response<ErrorResponse>(400, "Invalid request")
- @Response<ErrorResponse>(401, "Unauthorized")
- @Example<UserListResponse>({
- data: [
- {
- id: "550e8400-e29b-41d4-a716-446655440000",
- email: "john@example.com",
- name: "John Doe",
- status: UserStatus.Active,
- role: UserRole.User,
- createdAt: new Date("2024-01-15T10:30:00Z"),
- },
- ],
- pagination: {
- page: 1,
- limit: 20,
- total: 1,
- totalPages: 1,
- hasNext: false,
- hasPrev: false,
- },
- })
- public async listUsers(
- @Query() page: number = 1,
- @Query() limit: number = 20,
- @Query() status?: UserStatus,
- @Query() search?: string,
- ): Promise<UserListResponse> {
- // Implementation
- throw new Error("Not implemented");
- }
-
- /**
- * Create a new user
- */
- @Post()
- @Security("bearerAuth")
- @SuccessResponse(201, "Created")
- @Response<ErrorResponse>(400, "Invalid request")
- @Response<ErrorResponse>(409, "Email already exists")
- public async createUser(@Body() body: CreateUserRequest): Promise<User> {
- this.setStatus(201);
- throw new Error("Not implemented");
- }
-
- /**
- * Get user by ID
- * @param userId User ID
- */
- @Get("{userId}")
- @Security("bearerAuth")
- @Response<ErrorResponse>(404, "User not found")
- public async getUser(@Path() userId: string): Promise<User> {
- throw new Error("Not implemented");
- }
-
- /**
- * Update user attributes
- * @param userId User ID
- */
- @Patch("{userId}")
- @Security("bearerAuth")
- @Response<ErrorResponse>(400, "Invalid request")
- @Response<ErrorResponse>(404, "User not found")
- public async updateUser(
- @Path() userId: string,
- @Body() body: UpdateUserRequest,
- ): Promise<User> {
- throw new Error("Not implemented");
- }
-
- /**
- * Delete user
- * @param userId User ID
- */
- @Delete("{userId}")
- @Tags("Users", "Admin")
- @Security("bearerAuth")
- @SuccessResponse(204, "Deleted")
- @Response<ErrorResponse>(404, "User not found")
- public async deleteUser(@Path() userId: string): Promise<void> {
- this.setStatus(204);
- }
- }
- ```
-
- ### Template 4: Validation & Linting
-
- ```bash
- # Install validation tools
- npm install -g @stoplight/spectral-cli
- npm install -g @redocly/cli
-
- # Spectral ruleset (.spectral.yaml)
- cat > .spectral.yaml << 'EOF'
- extends: ["spectral:oas", "spectral:asyncapi"]
-
- rules:
- # Enforce operation IDs
- operation-operationId: error
-
- # Require descriptions
- operation-description: warn
- info-description: error
-
- # Naming conventions
- operation-operationId-valid-in-url: true
-
- # Security
- operation-security-defined: error
-
- # Response codes
- operation-success-response: error
-
- # Custom rules
- path-params-snake-case:
- description: Path parameters should be snake_case
- severity: warn
- given: "$.paths[*].parameters[?(@.in == 'path')].name"
- then:
- function: pattern
- functionOptions:
- match: "^[a-z][a-z0-9_]*$"
-
- schema-properties-camelCase:
- description: Schema properties should be camelCase
- severity: warn
- given: "$.components.schemas[*].properties[*]~"
- then:
- function: casing
- functionOptions:
- type: camel
- EOF
-
- # Run Spectral
- spectral lint openapi.yaml
-
- # Redocly config (redocly.yaml)
- cat > redocly.yaml << 'EOF'
- extends:
- - recommended
-
- rules:
- no-invalid-media-type-examples: error
- no-invalid-schema-examples: error
- operation-4xx-response: warn
- request-mime-type:
- severity: error
- allowedValues:
- - application/json
- response-mime-type:
- severity: error
- allowedValues:
- - application/json
- - application/problem+json
-
- theme:
- openapi:
- generateCodeSamples:
- languages:
- - lang: curl
- - lang: python
- - lang: javascript
- EOF
-
- # Run Redocly
- redocly lint openapi.yaml
- redocly bundle openapi.yaml -o bundled.yaml
- redocly preview-docs openapi.yaml
- ```
-
- ## SDK Generation
-
- ```bash
- # OpenAPI Generator
- npm install -g @openapitools/openapi-generator-cli
-
- # Generate TypeScript client
- openapi-generator-cli generate \
- -i openapi.yaml \
- -g typescript-fetch \
- -o ./generated/typescript-client \
- --additional-properties=supportsES6=true,npmName=@myorg/api-client
-
- # Generate Python client
- openapi-generator-cli generate \
- -i openapi.yaml \
- -g python \
- -o ./generated/python-client \
- --additional-properties=packageName=api_client
+ For advanced code-first generation patterns and tooling, see [references/code-first-and-tooling.md](references/code-first-and-tooling.md):
- # Generate Go client
- openapi-generator-cli generate \
- -i openapi.yaml \
- -g go \
- -o ./generated/go-client
- ```
+ - **Template 2: Python/FastAPI** — Pydantic models with `Field` validation, enum types, full CRUD endpoints with `response_model` and `status_code`, exporting the spec as JSON
+ - **Template 3: TypeScript/tsoa** — Decorator-based controllers (`@Route`, `@Get`, `@Security`, `@Example`, `@Response`) that generate OpenAPI from TypeScript types
+ - **Template 4: Validation & Linting** — Spectral ruleset (`.spectral.yaml`) with custom rules for operationId, security, naming conventions; Redocly config with MIME type enforcement and code sample generation
+ - **SDK Generation** — `openapi-generator-cli` for TypeScript (fetch), Python, and Go clients
## Best Practices
### Do's
- **Use $ref** - Reuse schemas, parameters, responses
- **Add examples** - Real-world values help consumers
- **Document errors** - All possible error codes
- **Version your API** - In URL or header
- **Use semantic versioning** - For spec changes
### Don'ts
- **Don't use generic descriptions** - Be specific
- **Don't skip security** - Define all schemes
- **Don't forget nullable** - Be explicit about null
- **Don't mix styles** - Consistent naming throughout
- **Don't hardcode URLs** - Use server variables