Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Generate OpenAPI 3.x specifications from existing code, routes, or descriptions with proper schemas and documentation.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/code-first-and-tooling.md
1# OpenAPI Code-First Generation and Tooling23Advanced patterns for generating OpenAPI specs from code (Python/FastAPI, TypeScript/tsoa), validation, linting, and SDK generation.45## Template 2: Code-First Generation (Python/FastAPI)67```python8# FastAPI with automatic OpenAPI generation9from fastapi import FastAPI, HTTPException, Query, Path, Depends10from pydantic import BaseModel, Field, EmailStr11from typing import Optional, List12from datetime import datetime13from uuid import UUID14from enum import Enum1516app = FastAPI(17title="User Management API",18description="API for managing users and profiles",19version="2.0.0",20openapi_tags=[21{"name": "Users", "description": "User operations"},22{"name": "Profiles", "description": "Profile operations"},23],24servers=[25{"url": "https://api.example.com/v2", "description": "Production"},26{"url": "http://localhost:8000", "description": "Development"},27],28)2930# Enums31class UserStatus(str, Enum):32active = "active"33inactive = "inactive"34suspended = "suspended"35pending = "pending"3637class UserRole(str, Enum):38user = "user"39moderator = "moderator"40admin = "admin"4142# Models43class UserBase(BaseModel):44email: EmailStr = Field(..., description="User email address")45name: str = Field(..., min_length=1, max_length=100, description="Display name")4647class UserCreate(UserBase):48role: UserRole = Field(default=UserRole.user)49metadata: Optional[dict] = Field(default=None, description="Custom metadata")5051model_config = {52"json_schema_extra": {53"examples": [54{55"email": "[email protected]",56"name": "John Doe",57"role": "user"58}59]60}61}6263class UserUpdate(BaseModel):64name: Optional[str] = Field(None, min_length=1, max_length=100)65status: Optional[UserStatus] = None66role: Optional[UserRole] = None67metadata: Optional[dict] = None6869class User(UserBase):70id: UUID = Field(..., description="Unique identifier")71status: UserStatus72role: UserRole73avatar: Optional[str] = Field(None, description="Avatar URL")74metadata: Optional[dict] = None75created_at: datetime = Field(..., alias="createdAt")76updated_at: Optional[datetime] = Field(None, alias="updatedAt")7778model_config = {"populate_by_name": True}7980class Pagination(BaseModel):81page: int = Field(..., ge=1)82limit: int = Field(..., ge=1, le=100)83total: int = Field(..., ge=0)84total_pages: int = Field(..., ge=0, alias="totalPages")85has_next: bool = Field(..., alias="hasNext")86has_prev: bool = Field(..., alias="hasPrev")8788class UserListResponse(BaseModel):89data: List[User]90pagination: Pagination9192class ErrorDetail(BaseModel):93field: str94message: str9596class ErrorResponse(BaseModel):97code: str = Field(..., description="Error code")98message: str = Field(..., description="Error message")99details: Optional[List[ErrorDetail]] = None100request_id: Optional[str] = Field(None, alias="requestId")101102# Endpoints103@app.get(104"/users",105response_model=UserListResponse,106tags=["Users"],107summary="List all users",108description="Returns a paginated list of users with optional filtering.",109responses={110400: {"model": ErrorResponse, "description": "Invalid request"},111401: {"model": ErrorResponse, "description": "Unauthorized"},112},113)114async def list_users(115page: int = Query(1, ge=1, description="Page number"),116limit: int = Query(20, ge=1, le=100, description="Items per page"),117status: Optional[UserStatus] = Query(None, description="Filter by status"),118search: Optional[str] = Query(None, min_length=2, max_length=100),119):120"""121List users with pagination and filtering.122123- **page**: Page number (1-based)124- **limit**: Number of items per page (max 100)125- **status**: Filter by user status126- **search**: Search by name or email127"""128# Implementation129pass130131@app.post(132"/users",133response_model=User,134status_code=201,135tags=["Users"],136summary="Create a new user",137responses={138400: {"model": ErrorResponse},139409: {"model": ErrorResponse, "description": "Email already exists"},140},141)142async def create_user(user: UserCreate):143"""Create a new user and send welcome email."""144pass145146@app.get(147"/users/{user_id}",148response_model=User,149tags=["Users"],150summary="Get user by ID",151responses={404: {"model": ErrorResponse}},152)153async def get_user(154user_id: UUID = Path(..., description="User ID"),155):156"""Retrieve a specific user by their ID."""157pass158159@app.patch(160"/users/{user_id}",161response_model=User,162tags=["Users"],163summary="Update user",164responses={165400: {"model": ErrorResponse},166404: {"model": ErrorResponse},167},168)169async def update_user(170user_id: UUID = Path(..., description="User ID"),171user: UserUpdate = ...,172):173"""Update user attributes."""174pass175176@app.delete(177"/users/{user_id}",178status_code=204,179tags=["Users", "Admin"],180summary="Delete user",181responses={404: {"model": ErrorResponse}},182)183async def delete_user(184user_id: UUID = Path(..., description="User ID"),185):186"""Permanently delete a user."""187pass188189# Export OpenAPI spec190if __name__ == "__main__":191import json192print(json.dumps(app.openapi(), indent=2))193```194195## Template 3: Code-First (TypeScript/Express with tsoa)196197```typescript198// tsoa generates OpenAPI from TypeScript decorators199200import {201Controller,202Get,203Post,204Patch,205Delete,206Route,207Path,208Query,209Body,210Response,211SuccessResponse,212Tags,213Security,214Example,215} from "tsoa";216217// Models218interface User {219/** Unique identifier */220id: string;221/** User email address */222email: string;223/** Display name */224name: string;225status: UserStatus;226role: UserRole;227/** Avatar URL */228avatar?: string;229/** Custom metadata */230metadata?: Record<string, unknown>;231createdAt: Date;232updatedAt?: Date;233}234235enum UserStatus {236Active = "active",237Inactive = "inactive",238Suspended = "suspended",239Pending = "pending",240}241242enum UserRole {243User = "user",244Moderator = "moderator",245Admin = "admin",246}247248interface CreateUserRequest {249email: string;250name: string;251role?: UserRole;252metadata?: Record<string, unknown>;253}254255interface UpdateUserRequest {256name?: string;257status?: UserStatus;258role?: UserRole;259metadata?: Record<string, unknown>;260}261262interface Pagination {263page: number;264limit: number;265total: number;266totalPages: number;267hasNext: boolean;268hasPrev: boolean;269}270271interface UserListResponse {272data: User[];273pagination: Pagination;274}275276interface ErrorResponse {277code: string;278message: string;279details?: { field: string; message: string }[];280requestId?: string;281}282283@Route("users")284@Tags("Users")285export class UsersController extends Controller {286/**287* List all users with pagination and filtering288* @param page Page number (1-based)289* @param limit Items per page (max 100)290* @param status Filter by user status291* @param search Search by name or email292*/293@Get()294@Security("bearerAuth")295@Response<ErrorResponse>(400, "Invalid request")296@Response<ErrorResponse>(401, "Unauthorized")297@Example<UserListResponse>({298data: [299{300id: "550e8400-e29b-41d4-a716-446655440000",301email: "[email protected]",302name: "John Doe",303status: UserStatus.Active,304role: UserRole.User,305createdAt: new Date("2024-01-15T10:30:00Z"),306},307],308pagination: {309page: 1,310limit: 20,311total: 1,312totalPages: 1,313hasNext: false,314hasPrev: false,315},316})317public async listUsers(318@Query() page: number = 1,319@Query() limit: number = 20,320@Query() status?: UserStatus,321@Query() search?: string,322): Promise<UserListResponse> {323// Implementation324throw new Error("Not implemented");325}326327/**328* Create a new user329*/330@Post()331@Security("bearerAuth")332@SuccessResponse(201, "Created")333@Response<ErrorResponse>(400, "Invalid request")334@Response<ErrorResponse>(409, "Email already exists")335public async createUser(@Body() body: CreateUserRequest): Promise<User> {336this.setStatus(201);337throw new Error("Not implemented");338}339340/**341* Get user by ID342* @param userId User ID343*/344@Get("{userId}")345@Security("bearerAuth")346@Response<ErrorResponse>(404, "User not found")347public async getUser(@Path() userId: string): Promise<User> {348throw new Error("Not implemented");349}350351/**352* Update user attributes353* @param userId User ID354*/355@Patch("{userId}")356@Security("bearerAuth")357@Response<ErrorResponse>(400, "Invalid request")358@Response<ErrorResponse>(404, "User not found")359public async updateUser(360@Path() userId: string,361@Body() body: UpdateUserRequest,362): Promise<User> {363throw new Error("Not implemented");364}365366/**367* Delete user368* @param userId User ID369*/370@Delete("{userId}")371@Tags("Users", "Admin")372@Security("bearerAuth")373@SuccessResponse(204, "Deleted")374@Response<ErrorResponse>(404, "User not found")375public async deleteUser(@Path() userId: string): Promise<void> {376this.setStatus(204);377}378}379```380381## Template 4: Validation & Linting382383```bash384# Install validation tools385npm install -g @stoplight/spectral-cli386npm install -g @redocly/cli387388# Spectral ruleset (.spectral.yaml)389cat > .spectral.yaml << 'EOF'390extends: ["spectral:oas", "spectral:asyncapi"]391392rules:393# Enforce operation IDs394operation-operationId: error395396# Require descriptions397operation-description: warn398info-description: error399400# Naming conventions401operation-operationId-valid-in-url: true402403# Security404operation-security-defined: error405406# Response codes407operation-success-response: error408409# Custom rules410path-params-snake-case:411description: Path parameters should be snake_case412severity: warn413given: "$.paths[*].parameters[?(@.in == 'path')].name"414then:415function: pattern416functionOptions:417match: "^[a-z][a-z0-9_]*$"418419schema-properties-camelCase:420description: Schema properties should be camelCase421severity: warn422given: "$.components.schemas[*].properties[*]~"423then:424function: casing425functionOptions:426type: camel427EOF428429# Run Spectral430spectral lint openapi.yaml431432# Redocly config (redocly.yaml)433cat > redocly.yaml << 'EOF'434extends:435- recommended436437rules:438no-invalid-media-type-examples: error439no-invalid-schema-examples: error440operation-4xx-response: warn441request-mime-type:442severity: error443allowedValues:444- application/json445response-mime-type:446severity: error447allowedValues:448- application/json449- application/problem+json450451theme:452openapi:453generateCodeSamples:454languages:455- lang: curl456- lang: python457- lang: javascript458EOF459460# Run Redocly461redocly lint openapi.yaml462redocly bundle openapi.yaml -o bundled.yaml463redocly preview-docs openapi.yaml464```465466## SDK Generation467468```bash469# OpenAPI Generator470npm install -g @openapitools/openapi-generator-cli471472# Generate TypeScript client473openapi-generator-cli generate \474-i openapi.yaml \475-g typescript-fetch \476-o ./generated/typescript-client \477--additional-properties=supportsES6=true,npmName=@myorg/api-client478479# Generate Python client480openapi-generator-cli generate \481-i openapi.yaml \482-g python \483-o ./generated/python-client \484--additional-properties=packageName=api_client485486# Generate Go client487openapi-generator-cli generate \488-i openapi.yaml \489-g go \490-o ./generated/go-client491```492