<< All versions
Skill v1.0.0
currentAutomated scan100/100bbridgeers/hermes-skills/openapi-spec
──Details
PublishedOctober 3, 2026 at 06:23 PM
Content Hashsha256:3f31a82f43909b18...
Git SHA
──Files
Files (1 file, 24.1 KB)
SKILL.md24.1 KBactive
SKILL.md · 1030 lines · 24.1 KB
version: "1.0.0" 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.0info:title: API Titleversion: 1.0.0servers:- url: https://api.example.com/v1paths:/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.0info:title: User Management APIdescription: |API for managing users and their profiles.## AuthenticationAll endpoints require Bearer token authentication.## Rate Limiting- 1000 requests per minute for standard tier- 10000 requests per minute for enterprise tierversion: 2.0.0contact:name: API Supportemail: api-support@example.comurl: https://docs.example.comlicense:name: MITurl: https://opensource.org/licenses/MITservers:- url: https://api.example.com/v2description: Production- url: https://staging-api.example.com/v2description: Staging- url: http://localhost:3000/v2description: Local developmenttags:- name: Usersdescription: User management operations- name: Profilesdescription: User profile operations- name: Admindescription: Administrative operationspaths:/users:get:operationId: listUserssummary: List all usersdescription: Returns a paginated list of users with optional filtering.tags:- Usersparameters:- $ref: '#/components/parameters/PageParam'- $ref: '#/components/parameters/LimitParam'- name: statusin: querydescription: Filter by user statusschema:$ref: '#/components/schemas/UserStatus'- name: searchin: querydescription: Search by name or emailschema:type: stringminLength: 2maxLength: 100responses:'200':description: Successful responsecontent: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: createUsersummary: Create a new userdescription: Creates a new user account and sends welcome email.tags:- UsersrequestBody:required: truecontent:application/json:schema:$ref: '#/components/schemas/CreateUserRequest'examples:standard:summary: Standard uservalue:email: user@example.comname: John Doerole: useradmin:summary: Admin uservalue:email: admin@example.comname: Admin Userrole: adminresponses:'201':description: User created successfullycontent:application/json:schema:$ref: '#/components/schemas/User'headers:Location:description: URL of created userschema:type: stringformat: uri'400':$ref: '#/components/responses/BadRequest''409':description: Email already existscontent:application/json:schema:$ref: '#/components/schemas/Error'security:- bearerAuth: []/users/{userId}:parameters:- $ref: '#/components/parameters/UserIdParam'get:operationId: getUsersummary: Get user by IDtags:- Usersresponses:'200':description: Successful responsecontent:application/json:schema:$ref: '#/components/schemas/User''404':$ref: '#/components/responses/NotFound'security:- bearerAuth: []patch:operationId: updateUsersummary: Update usertags:- UsersrequestBody:required: truecontent:application/json:schema:$ref: '#/components/schemas/UpdateUserRequest'responses:'200':description: User updatedcontent:application/json:schema:$ref: '#/components/schemas/User''400':$ref: '#/components/responses/BadRequest''404':$ref: '#/components/responses/NotFound'security:- bearerAuth: []delete:operationId: deleteUsersummary: Delete usertags:- Users- Adminresponses:'204':description: User deleted'404':$ref: '#/components/responses/NotFound'security:- bearerAuth: []- apiKey: []components:schemas:User:type: objectrequired:- id- name- status- createdAtproperties:id:type: stringformat: uuidreadOnly: truedescription: Unique user identifieremail:type: stringformat: emaildescription: User email addressname:type: stringminLength: 1maxLength: 100description: User display namestatus:$ref: '#/components/schemas/UserStatus'role:type: stringenum: [user, moderator, admin]default: useravatar:type: stringformat: urinullable: truemetadata:type: objectadditionalProperties: truedescription: Custom metadatacreatedAt:type: stringformat: date-timereadOnly: trueupdatedAt:type: stringformat: date-timereadOnly: trueUserStatus:type: stringenum: [active, inactive, suspended, pending]description: User account statusCreateUserRequest:type: objectrequired:- nameproperties:email:type: stringformat: emailname:type: stringminLength: 1maxLength: 100role:type: stringenum: [user, moderator, admin]default: usermetadata:type: objectadditionalProperties: trueUpdateUserRequest:type: objectminProperties: 1properties:name:type: stringminLength: 1maxLength: 100status:$ref: '#/components/schemas/UserStatus'role:type: stringenum: [user, moderator, admin]metadata:type: objectadditionalProperties: trueUserListResponse:type: objectrequired:- data- paginationproperties:data:type: arrayitems:$ref: '#/components/schemas/User'pagination:$ref: '#/components/schemas/Pagination'Pagination:type: objectrequired:- page- limit- total- totalPagesproperties:page:type: integerminimum: 1limit:type: integerminimum: 1maximum: 100total:type: integerminimum: 0totalPages:type: integerminimum: 0hasNext:type: booleanhasPrev:type: booleanError:type: objectrequired:- code- messageproperties:code:type: stringdescription: Error code for programmatic handlingmessage:type: stringdescription: Human-readable error messagedetails:type: arrayitems:type: objectproperties:field:type: stringmessage:type: stringrequestId:type: stringdescription: Request ID for supportparameters:UserIdParam:name: userIdin: pathrequired: truedescription: User IDschema:type: stringformat: uuidPageParam:name: pagein: querydescription: Page number (1-based)schema:type: integerminimum: 1default: 1LimitParam:name: limitin: querydescription: Items per pageschema:type: integerminimum: 1maximum: 100default: 20responses:BadRequest:description: Invalid requestcontent:application/json:schema:$ref: '#/components/schemas/Error'example:code: VALIDATION_ERRORmessage: Invalid request parametersdetails:- field: emailmessage: Must be a valid email addressUnauthorized:description: Authentication requiredcontent:application/json:schema:$ref: '#/components/schemas/Error'example:code: UNAUTHORIZEDmessage: Authentication requiredNotFound:description: Resource not foundcontent:application/json:schema:$ref: '#/components/schemas/Error'example:code: NOT_FOUNDmessage: User not foundRateLimited:description: Too many requestscontent:application/json:schema:$ref: '#/components/schemas/Error'headers:Retry-After:description: Seconds until rate limit resetsschema:type: integerX-RateLimit-Limit:description: Request limit per windowschema:type: integerX-RateLimit-Remaining:description: Remaining requests in windowschema:type: integerexamples: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: 1limit: 20total: 1totalPages: 1hasNext: falsehasPrev: falsesecuritySchemes:bearerAuth:type: httpscheme: bearerbearerFormat: JWTdescription: JWT token from /auth/loginapiKey:type: apiKeyin: headername: X-API-Keydescription: API key for service-to-service callssecurity:- bearerAuth: []
Template 2: Code-First Generation (Python/FastAPI)
python
# FastAPI with automatic OpenAPI generationfrom fastapi import FastAPI, HTTPException, Query, Path, Dependsfrom pydantic import BaseModel, Field, EmailStrfrom typing import Optional, Listfrom datetime import datetimefrom uuid import UUIDfrom enum import Enumapp = 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"},],)# Enumsclass UserStatus(str, Enum):active = "active"inactive = "inactive"suspended = "suspended"pending = "pending"class UserRole(str, Enum):user = "user"moderator = "moderator"admin = "admin"# Modelsclass 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] = Nonerole: Optional[UserRole] = Nonemetadata: Optional[dict] = Noneclass User(UserBase):id: UUID = Field(..., description="Unique identifier")status: UserStatusrole: UserRoleavatar: Optional[str] = Field(None, description="Avatar URL")metadata: Optional[dict] = Nonecreated_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: Paginationclass ErrorDetail(BaseModel):field: strmessage: strclass ErrorResponse(BaseModel):code: str = Field(..., description="Error code")message: str = Field(..., description="Error message")details: Optional[List[ErrorDetail]] = Nonerequest_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"""# Implementationpass@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 specif __name__ == "__main__":import jsonprint(json.dumps(app.openapi(), indent=2))
Template 3: Code-First (TypeScript/Express with tsoa)
typescript
// tsoa generates OpenAPI from TypeScript decoratorsimport {Controller,Get,Post,Patch,Delete,Route,Path,Query,Body,Response,SuccessResponse,Tags,Security,Example,} from "tsoa";// Modelsinterface 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> {// Implementationthrow 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 toolsnpm install -g @stoplight/spectral-clinpm install -g @redocly/cli# Spectral ruleset (.spectral.yaml)cat > .spectral.yaml << 'EOF'extends: ["spectral:oas", "spectral:asyncapi"]rules:# Enforce operation IDsoperation-operationId: error# Require descriptionsoperation-description: warninfo-description: error# Naming conventionsoperation-operationId-valid-in-url: true# Securityoperation-security-defined: error# Response codesoperation-success-response: error# Custom rulespath-params-snake-case:description: Path parameters should be snake_caseseverity: warngiven: "$.paths[*].parameters[?(@.in == 'path')].name"then:function: patternfunctionOptions:match: "^[a-z][a-z0-9_]*$"schema-properties-camelCase:description: Schema properties should be camelCaseseverity: warngiven: "$.components.schemas[*].properties[*]~"then:function: casingfunctionOptions:type: camelEOF# Run Spectralspectral lint openapi.yaml# Redocly config (redocly.yaml)cat > redocly.yaml << 'EOF'extends:- recommendedrules:no-invalid-media-type-examples: errorno-invalid-schema-examples: erroroperation-4xx-response: warnrequest-mime-type:severity: errorallowedValues:- application/jsonresponse-mime-type:severity: errorallowedValues:- application/json- application/problem+jsontheme:openapi:generateCodeSamples:languages:- lang: curl- lang: python- lang: javascriptEOF# Run Redoclyredocly lint openapi.yamlredocly bundle openapi.yaml -o bundled.yamlredocly preview-docs openapi.yaml
SDK Generation
bash
# OpenAPI Generatornpm install -g @openapitools/openapi-generator-cli# Generate TypeScript clientopenapi-generator-cli generate \-i openapi.yaml \-g typescript-fetch \-o ./generated/typescript-client \--additional-properties=supportsES6=true,npmName=@myorg/api-client# Generate Python clientopenapi-generator-cli generate \-i openapi.yaml \-g python \-o ./generated/python-client \--additional-properties=packageName=api_client# Generate Go clientopenapi-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