--- 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; 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; } interface UpdateUserRequest { name?: string; status?: UserStatus; role?: UserRole; metadata?: Record; } 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(400, "Invalid request") @Response(401, "Unauthorized") @Example({ 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 { // Implementation throw new Error("Not implemented"); } /** * Create a new user */ @Post() @Security("bearerAuth") @SuccessResponse(201, "Created") @Response(400, "Invalid request") @Response(409, "Email already exists") public async createUser( @Body() body: CreateUserRequest ): Promise { this.setStatus(201); throw new Error("Not implemented"); } /** * Get user by ID * @param userId User ID */ @Get("{userId}") @Security("bearerAuth") @Response(404, "User not found") public async getUser( @Path() userId: string ): Promise { throw new Error("Not implemented"); } /** * Update user attributes * @param userId User ID */ @Patch("{userId}") @Security("bearerAuth") @Response(400, "Invalid request") @Response(404, "User not found") public async updateUser( @Path() userId: string, @Body() body: UpdateUserRequest ): Promise { throw new Error("Not implemented"); } /** * Delete user * @param userId User ID */ @Delete("{userId}") @Tags("Users", "Admin") @Security("bearerAuth") @SuccessResponse(204, "Deleted") @Response(404, "User not found") public async deleteUser( @Path() userId: string ): Promise { 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 # Generate Go client openapi-generator-cli generate \ -i openapi.yaml \ -g go \ -o ./generated/go-client ``` ## 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 ## Resources - [OpenAPI 3.1 Specification](https://spec.openapis.org/oas/v3.1.0) - [Swagger Editor](https://editor.swagger.io/) - [Redocly](https://redocly.com/) - [Spectral](https://stoplight.io/open-source/spectral)