Files
claudekit/.claude/skills/api/openapi/SKILL.md
T

838 lines
21 KiB
Markdown

---
name: openapi
description: >
Use this skill when designing, documenting, or generating REST API specifications using OpenAPI/Swagger. Trigger on keywords like OpenAPI, Swagger, API spec, REST documentation, API schema, request body, response schema, and API client generation. Also apply when adopting design-first API development, validating API contracts, or setting up auto-generated API documentation for FastAPI, Express, or NestJS endpoints.
---
# OpenAPI & REST API Design
## When to Use
- Documenting REST APIs
- Generating API clients
- API design-first development
- Defining webhook contracts
- Establishing pagination, versioning, or auth patterns for a new service
## When NOT to Use
- Internal-only scripts or automation that do not expose HTTP endpoints
- CLI tools and command-line utilities without a REST interface
- GraphQL APIs where a different specification format applies
---
## Core Patterns
### 1. OpenAPI 3.1 Specification Structure
A complete spec skeleton showing every top-level section. Use `$ref` to split
large specs into per-resource files.
```yaml
openapi: 3.1.0
info:
title: Acme API
version: 2.0.0
description: Public API for the Acme platform.
contact:
name: API Support
email: api@acme.dev
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: https://api.acme.dev/v2
description: Production
- url: https://staging-api.acme.dev/v2
description: Staging
tags:
- name: Users
description: User management operations
- name: Orders
description: Order lifecycle operations
paths:
/users:
$ref: './paths/users.yaml'
/users/{userId}:
$ref: './paths/users-by-id.yaml'
/orders:
$ref: './paths/orders.yaml'
components:
schemas:
$ref: './components/schemas/_index.yaml'
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
security:
- BearerAuth: []
webhooks:
orderCompleted:
$ref: './webhooks/order-completed.yaml'
```
**Organizing with `$ref`** -- keep one file per resource under `paths/` and
shared schemas under `components/schemas/`. A bundler such as
`@redocly/cli bundle` resolves references into a single file for tooling.
```
spec/
├── openapi.yaml # Root document
├── paths/
│ ├── users.yaml
│ ├── users-by-id.yaml
│ └── orders.yaml
├── components/
│ └── schemas/
│ ├── _index.yaml
│ ├── User.yaml
│ ├── Order.yaml
│ └── ProblemDetail.yaml
└── webhooks/
└── order-completed.yaml
```
---
### 2. Path & Operation Patterns
#### RESTful URL Naming Conventions
- Use **plural nouns** for collections: `/users`, `/orders`.
- Use **path parameters** for single-resource access: `/users/{userId}`.
- Nest only one level deep: `/users/{userId}/orders` (not deeper).
- Use **query parameters** for filtering, sorting, and pagination.
- Avoid verbs in paths -- let HTTP methods convey the action.
#### CRUD Operations
```yaml
paths:
/users:
get:
operationId: listUsers
tags: [Users]
summary: List users
parameters:
- $ref: '#/components/parameters/PageCursor'
- $ref: '#/components/parameters/PageSize'
- name: status
in: query
schema:
type: string
enum: [active, inactive]
responses:
'200':
description: Paginated list of users
content:
application/json:
schema:
$ref: '#/components/schemas/UserListResponse'
post:
operationId: createUser
tags: [Users]
summary: Create a user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created
headers:
Location:
schema:
type: string
description: URL of the new resource
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
/users/{userId}:
parameters:
- name: userId
in: path
required: true
schema:
type: string
format: uuid
get:
operationId: getUser
tags: [Users]
summary: Get a single user
responses:
'200':
description: User found
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
$ref: '#/components/responses/NotFound'
patch:
operationId: updateUser
tags: [Users]
summary: Partially update a user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateUserRequest'
responses:
'200':
description: User updated
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
$ref: '#/components/responses/NotFound'
'422':
$ref: '#/components/responses/ValidationError'
delete:
operationId: deleteUser
tags: [Users]
summary: Delete a user
responses:
'204':
description: User deleted
'404':
$ref: '#/components/responses/NotFound'
```
#### Path Parameters vs Query Parameters
| Use case | Mechanism | Example |
|----------|-----------|---------|
| Identify a specific resource | Path parameter | `/orders/{orderId}` |
| Filter a collection | Query parameter | `/orders?status=shipped` |
| Sort a collection | Query parameter | `/orders?sort=-createdAt` |
| Paginate | Query parameter | `/orders?cursor=abc&limit=20` |
| Expand nested data | Query parameter | `/orders?expand=items,customer` |
---
### 3. Request Body Patterns
#### JSON Request Body with Validation
```yaml
components:
schemas:
CreateUserRequest:
type: object
required:
- email
- name
properties:
email:
type: string
format: email
maxLength: 254
name:
type: string
minLength: 1
maxLength: 100
role:
type: string
enum: [admin, member, viewer]
default: member
additionalProperties: false
```
Implementation in **FastAPI** (Python):
```python
from pydantic import BaseModel, EmailStr, Field
class CreateUserRequest(BaseModel):
email: EmailStr
name: str = Field(min_length=1, max_length=100)
role: str = Field(default="member", pattern="^(admin|member|viewer)$")
model_config = {"extra": "forbid"}
```
Implementation in **Express** (TypeScript with Zod):
```typescript
import { z } from "zod";
const CreateUserRequest = z.object({
email: z.string().email().max(254),
name: z.string().min(1).max(100),
role: z.enum(["admin", "member", "viewer"]).default("member"),
}).strict();
type CreateUserRequest = z.infer<typeof CreateUserRequest>;
```
#### Multipart Form Data (File Uploads)
```yaml
/users/{userId}/avatar:
put:
operationId: uploadAvatar
tags: [Users]
summary: Upload user avatar
parameters:
- name: userId
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required:
- file
properties:
file:
type: string
format: binary
description: Image file (JPEG or PNG, max 5 MB)
caption:
type: string
maxLength: 200
encoding:
file:
contentType: image/jpeg, image/png
responses:
'200':
description: Avatar updated
content:
application/json:
schema:
type: object
properties:
url:
type: string
format: uri
'413':
$ref: '#/components/responses/PayloadTooLarge'
```
#### Content Negotiation
Support multiple response formats by listing them under `content`:
```yaml
responses:
'200':
description: Export data
content:
application/json:
schema:
$ref: '#/components/schemas/ExportData'
text/csv:
schema:
type: string
application/pdf:
schema:
type: string
format: binary
```
Clients select a format with the `Accept` header. Document which formats your
API actually supports so consumers do not have to guess.
---
### 4. Response Patterns
#### Success Responses
| Code | Meaning | Typical use |
|------|---------|-------------|
| `200` | OK | GET, PATCH, general success |
| `201` | Created | POST that creates a resource |
| `202` | Accepted | Async operation started |
| `204` | No Content | DELETE, or PUT with no body returned |
Always return a `Location` header with `201` pointing to the new resource.
#### Error Responses -- RFC 7807 Problem Details
Define a single reusable error schema based on RFC 7807:
```yaml
components:
schemas:
ProblemDetail:
type: object
required:
- type
- title
- status
properties:
type:
type: string
format: uri
description: URI reference identifying the problem type.
example: https://api.acme.dev/problems/validation-error
title:
type: string
description: Short human-readable summary.
example: Validation Error
status:
type: integer
description: HTTP status code.
example: 422
detail:
type: string
description: Human-readable explanation specific to this occurrence.
example: "Field 'email' must be a valid email address."
instance:
type: string
format: uri
description: URI identifying this specific occurrence.
errors:
type: array
description: Field-level validation errors (optional extension).
items:
type: object
properties:
field:
type: string
example: email
message:
type: string
example: Must be a valid email address.
code:
type: string
example: invalid_format
responses:
NotFound:
description: Resource not found
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
example:
type: https://api.acme.dev/problems/not-found
title: Not Found
status: 404
detail: User with ID '550e8400' was not found.
ValidationError:
description: Request validation failed
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
example:
type: https://api.acme.dev/problems/validation-error
title: Validation Error
status: 422
errors:
- field: email
message: Must be a valid email address.
code: invalid_format
Conflict:
description: Resource conflict
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
PayloadTooLarge:
description: Request payload exceeds limit
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
```
Use the `application/problem+json` media type for all error responses to signal
RFC 7807 compliance.
---
### 5. Authentication Schemes
#### Bearer Token (JWT)
```yaml
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- BearerAuth: []
```
Override per-operation to allow unauthenticated access:
```yaml
paths:
/health:
get:
security: [] # No auth required
responses:
'200':
description: Healthy
```
#### API Key
```yaml
components:
securitySchemes:
ApiKeyHeader:
type: apiKey
in: header
name: X-API-Key
ApiKeyQuery:
type: apiKey
in: query
name: api_key
```
#### OAuth2 Flows
```yaml
components:
securitySchemes:
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.acme.dev/authorize
tokenUrl: https://auth.acme.dev/token
refreshUrl: https://auth.acme.dev/token
scopes:
users:read: Read user profiles
users:write: Create and update users
orders:read: Read orders
paths:
/users:
get:
security:
- OAuth2: [users:read]
```
---
### 6. Pagination Patterns
#### Cursor-Based Pagination (Recommended)
Best for large, real-time datasets where rows may be inserted or deleted
between pages.
```yaml
components:
parameters:
PageCursor:
name: cursor
in: query
description: Opaque cursor returned by a previous response.
schema:
type: string
PageSize:
name: limit
in: query
description: Maximum items per page.
schema:
type: integer
minimum: 1
maximum: 100
default: 20
schemas:
UserListResponse:
type: object
required:
- data
- pagination
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
pagination:
type: object
required:
- hasMore
properties:
nextCursor:
type: string
nullable: true
hasMore:
type: boolean
```
#### Offset-Based Pagination
Simpler but less efficient for large tables and susceptible to drift when data
changes between requests.
```yaml
components:
parameters:
PageOffset:
name: offset
in: query
schema:
type: integer
minimum: 0
default: 0
PageLimit:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
schemas:
PaginatedResponse:
type: object
required:
- data
- total
- offset
- limit
properties:
data:
type: array
items: {}
total:
type: integer
description: Total number of matching records.
offset:
type: integer
limit:
type: integer
```
#### Response Envelope Pattern
Wrap every collection in a consistent envelope so clients always know where to
find the data and metadata:
```json
{
"data": [ ... ],
"pagination": { "nextCursor": "abc123", "hasMore": true },
"meta": { "requestId": "req_xyz", "timestamp": "2026-03-29T12:00:00Z" }
}
```
---
### 7. API Versioning
#### URL Versioning
```yaml
servers:
- url: https://api.acme.dev/v1
description: Version 1 (deprecated)
- url: https://api.acme.dev/v2
description: Version 2 (current)
```
Pros: explicit, easy to route, cache-friendly.
Cons: duplicates paths across versions, harder to share schemas.
#### Header Versioning
```yaml
parameters:
- name: X-API-Version
in: header
required: false
schema:
type: string
enum: ['2024-01-15', '2025-06-01']
default: '2025-06-01'
description: Date-based API version. Defaults to latest stable.
```
Pros: clean URLs, fine-grained control.
Cons: less discoverable, harder to test in a browser.
#### Trade-offs Summary
| Approach | Discoverability | URL cleanliness | Caching | Migration effort |
|----------|----------------|-----------------|---------|-----------------|
| URL path | High | Lower | Easy | Higher (path changes) |
| Header | Lower | High | Needs Vary header | Lower |
| Query param | Medium | Medium | Easy | Lower |
Pick one approach and use it consistently. URL versioning is the most common
choice for public APIs; header versioning suits internal services.
---
### 8. Webhook Specifications
OpenAPI 3.1 supports a top-level `webhooks` key for documenting outbound
event payloads your API will send to consumer-registered URLs.
```yaml
webhooks:
orderCompleted:
post:
operationId: onOrderCompleted
summary: Fired when an order reaches "completed" status.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OrderCompletedEvent'
responses:
'200':
description: Webhook received successfully.
components:
schemas:
WebhookEventBase:
type: object
required:
- id
- type
- createdAt
properties:
id:
type: string
format: uuid
type:
type: string
createdAt:
type: string
format: date-time
OrderCompletedEvent:
allOf:
- $ref: '#/components/schemas/WebhookEventBase'
- type: object
required:
- data
properties:
type:
type: string
const: order.completed
data:
type: object
properties:
orderId:
type: string
format: uuid
total:
type: number
format: double
currency:
type: string
example: USD
```
Document a shared `WebhookEventBase` so all event payloads have a consistent
envelope with `id`, `type`, and `createdAt`.
---
## Best Practices
1. **Use consistent, plural resource names.** `/users`, `/orders`, `/invoices`
-- never mix singular and plural within the same API.
2. **Make mutating operations idempotent.** Accept an `Idempotency-Key` header
on POST endpoints so clients can safely retry without creating duplicates.
3. **Return rate-limit headers on every response.** Include `X-RateLimit-Limit`,
`X-RateLimit-Remaining`, and `X-RateLimit-Reset` so clients can self-throttle.
4. **Provide `operationId` for every operation.** Code generators use this as
the method name; without it, generated clients have meaningless names.
5. **Include realistic examples in the spec.** Examples power documentation UIs,
mock servers, and contract tests. Add them at both the schema and operation
level.
6. **Use `additionalProperties: false` on request schemas.** This catches typos
in client payloads early and prevents silently ignored fields.
7. **Document hypermedia links (HATEOAS basics).** Even a minimal `_links`
object with `self` and `next` URIs helps clients navigate without hardcoding
paths.
8. **Version your spec file alongside code.** Store the OpenAPI document in the
same repository as the implementation. Run a CI check (e.g., `redocly lint`)
to validate the spec on every pull request.
---
## Common Pitfalls
1. **Missing error documentation.** Every operation should list its possible
`4xx` and `5xx` responses. Consumers cannot handle errors they do not know
about. At minimum document `400`, `401`, `403`, `404`, and `500`.
2. **Overusing `200 OK` for everything.** Return `201` for resource creation,
`204` for deletion, and `202` for asynchronous actions. Correct status codes
let generic HTTP clients behave properly (e.g., following `Location` headers).
3. **Deeply nested resource URLs.** `/users/{uid}/orders/{oid}/items/{iid}/notes`
is fragile and hard to cache. Flatten to `/order-items/{iid}/notes` once the
relationship is established.
4. **Inconsistent naming conventions.** Mixing `camelCase` and `snake_case`
within the same API confuses consumers. Pick one JSON field casing and enforce
it with a linter rule.
5. **Ignoring `nullable` vs optional.** In OpenAPI 3.1, `nullable` is gone;
use `type: ["string", "null"]` instead. A field that is not in `required`
may be absent, but that is different from being explicitly `null`. Be precise
about which you intend.
6. **No pagination on list endpoints.** Returning unbounded arrays will
eventually cause timeouts or OOM errors. Every collection endpoint should
accept `limit` and either `cursor` or `offset` from day one, even if the
dataset is currently small.
---
## Related Skills
- `patterns/api-client` - Patterns for consuming and generating API clients from specs
- `patterns/error-handling` - Consistent error response structures and handling
- `frameworks/fastapi` - FastAPI framework with built-in OpenAPI generation