api-design-patterns

API contract design conventions for FastAPI projects with Pydantic v2. Use during the design phase when planning new API endpoints, defining request/response contracts, designing pagination or filtering, standardizing error responses, or planning API versioning. Covers RESTful naming, HTTP method semantics, Pydantic v2 schema naming conventions (XxxCreate/XxxUpdate/XxxResponse), cursor-based pagination, standard error format, and OpenAPI documentation. Does NOT cover implementation details (use python-backend-expert) or system-level architecture (use system-architecture).

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "api-design-patterns" with this command: npx skills add hieutrtr/ai1-skills/hieutrtr-ai1-skills-api-design-patterns

API Design Patterns

When to Use

Activate this skill when:

  • Designing new API endpoints or modifying existing endpoint contracts
  • Defining request/response schemas for a feature
  • Standardizing pagination, filtering, or sorting across endpoints
  • Designing a consistent error response format
  • Planning API versioning or deprecation strategy
  • Reviewing API contracts for consistency before implementation
  • Documenting endpoint specifications for frontend/backend coordination

Input: If plan.md or architecture.md exists, read for context about the feature scope and architectural decisions. Otherwise, work from the user's request directly.

Output: Write API design to api-design.md. Tell the user: "API design written to api-design.md. Run /task-decomposition to create implementation tasks or /python-backend-expert to implement."

Do NOT use this skill for:

  • Writing implementation code (use python-backend-expert)
  • System-level architecture decisions (use system-architecture)
  • Writing tests for endpoints (use pytest-patterns)
  • Frontend data fetching implementation (use react-frontend-expert)

Instructions

URL Naming Conventions

Resource Naming Rules

  1. Plural nouns for collections: /users, /orders, /products
  2. Kebab-case for multi-word resources: /order-items, /user-profiles
  3. Singular resource by ID: /users/{user_id}, /orders/{order_id}
  4. Maximum 2 nesting levels: /users/{user_id}/orders (not /users/{user_id}/orders/{order_id}/items/{item_id})
  5. No verbs in URLs: use HTTP methods instead (POST /orders not /orders/create)
  6. Query parameters for filtering, sorting, pagination: /users?role=admin&sort=-created_at

URL Structure Template

/{version}/{resource}                    → Collection (list, create)
/{version}/{resource}/{id}               → Single resource (get, update, delete)
/{version}/{resource}/{id}/{sub-resource} → Nested collection
/{version}/{resource}/actions/{action}   → Non-CRUD operations (rarely needed)

Naming Examples

GoodBadReason
GET /v1/usersGET /v1/getUsersNo verbs — HTTP method implies action
POST /v1/usersPOST /v1/user/createPOST to collection = create
GET /v1/order-itemsGET /v1/orderItemsKebab-case, not camelCase
GET /v1/users/{id}/ordersGET /v1/users/{id}/orders/{oid}/itemsMax 2 nesting levels
POST /v1/orders/{id}/actions/cancelPOST /v1/cancelOrder/{id}Action sub-resource for non-CRUD

HTTP Method Semantics

MethodPurposeRequest BodySuccess StatusIdempotent
GETRetrieve resource(s)None200 OKYes
POSTCreate new resourceRequired201 CreatedNo
PUTFull replaceRequired (full)200 OKYes
PATCHPartial updateRequired (partial)200 OKNo*
DELETERemove resourceNone204 No ContentYes

*PATCH is not inherently idempotent but can be made so with proper implementation.

Response headers for creation:

  • POST returning 201 SHOULD include a Location header with the URL of the created resource

Conditional requests:

  • Support If-None-Match / ETag for caching on GET endpoints with frequently-accessed resources

Schema Naming Conventions (Pydantic v2)

Follow a consistent naming pattern for all Pydantic schemas:

PatternPurposeFields
{Resource}CreatePOST request bodyWritable fields, no id, no timestamps
{Resource}UpdatePUT request bodyAll writable fields required
{Resource}PatchPATCH request bodyAll fields Optional
{Resource}ResponseSingle resource responseAll fields including id, timestamps
{Resource}ListResponsePaginated list responseitems + pagination metadata
{Resource}FilterQuery parametersOptional filter fields

Schema design rules:

  • Never expose internal fields (hashed_password, internal_notes) in Response schemas
  • Always include id and timestamps (created_at, updated_at) in Response schemas
  • Use model_validate(orm_instance) to convert ORM models to response schemas
  • Use model_dump(exclude_unset=True) for PATCH operations to distinguish "not provided" from "set to null"
  • Reference references/pydantic-schema-examples.md for concrete examples

Pagination

Cursor-Based Pagination (Default)

Use cursor-based pagination for all list endpoints. It is more performant than offset-based for large datasets and avoids the "shifting window" problem.

Request parameters:

GET /v1/users?cursor=eyJpZCI6MTAwfQ&limit=20
ParameterTypeDefaultDescription
cursorstr | NoneNoneOpaque cursor from previous response
limitint20Items per page (max 100)

Response format:

{
  "items": [...],
  "next_cursor": "eyJpZCI6MTIwfQ",
  "has_more": true
}

Cursor implementation:

  • Encode the last item's sort key (usually id) as a base64 string
  • The cursor is opaque to the client — they must not parse or construct it
  • Use WHERE id > :last_id ORDER BY id ASC LIMIT :limit + 1 — fetch one extra to determine has_more

Offset-Based Pagination (When Needed)

Use offset-based only when the client needs to jump to arbitrary pages (e.g., admin tables).

{
  "items": [...],
  "total": 150,
  "page": 2,
  "page_size": 20,
  "total_pages": 8
}

Filtering and Sorting

Filtering

Use query parameters with field names:

GET /v1/users?role=admin&is_active=true&created_after=2024-01-01

Filtering conventions:

  • Exact match: ?field=value
  • Range: ?field_min=10&field_max=100 or ?created_after=...&created_before=...
  • Search: ?q=search+term (for full-text search across multiple fields)
  • Multiple values: ?status=active&status=pending (OR semantics)

Sorting

Use a sort query parameter with field name and direction prefix:

GET /v1/users?sort=-created_at        → descending by created_at
GET /v1/users?sort=name               → ascending by name
GET /v1/users?sort=-created_at,name   → multi-field sort

Convention: - prefix means descending, no prefix means ascending.

Error Response Format

All API errors follow a consistent format:

{
  "detail": "Human-readable error message",
  "code": "MACHINE_READABLE_CODE",
  "field_errors": [
    {
      "field": "email",
      "message": "Invalid email format",
      "code": "INVALID_FORMAT"
    }
  ]
}

Standard Error Codes and Status Mapping

HTTP StatusWhen to UseExample code
400Malformed requestBAD_REQUEST
401Missing or invalid authenticationUNAUTHORIZED
403Authenticated but not authorizedFORBIDDEN
404Resource not foundNOT_FOUND
409Conflict (duplicate, version mismatch)CONFLICT
422Validation error (Pydantic)VALIDATION_ERROR
429Rate limit exceededRATE_LIMITED
500Unexpected server errorINTERNAL_ERROR

Error schema (Pydantic v2):

class FieldError(BaseModel):
    field: str
    message: str
    code: str

class ErrorResponse(BaseModel):
    detail: str
    code: str
    field_errors: list[FieldError] = []

API Versioning

Strategy: URL Prefix Versioning

/v1/users    → Version 1
/v2/users    → Version 2

Versioning rules:

  1. Start with /v1/ for all new APIs
  2. Increment major version only for breaking changes
  3. Non-breaking changes (new optional fields, new endpoints) do NOT require a new version
  4. Support at most 2 active versions simultaneously

Breaking changes that require a new version:

  • Removing a field from a response
  • Changing a field's type
  • Making an optional request field required
  • Changing the URL structure for existing endpoints
  • Changing error response format

Deprecation process:

  1. Add Deprecation header to the old version: Deprecation: true
  2. Add Sunset header with the retirement date: Sunset: Sat, 01 Mar 2026 00:00:00 GMT
  3. Add Link header pointing to the new version: Link: </v2/users>; rel="successor-version"
  4. Log usage of deprecated endpoints for monitoring
  5. Remove the old version after the sunset date

OpenAPI Documentation

FastAPI generates OpenAPI schemas automatically. Enhance them with:

@router.get(
    "/users/{user_id}",
    response_model=UserResponse,
    summary="Get user by ID",
    description="Retrieve a single user's details by their unique identifier.",
    responses={
        404: {"model": ErrorResponse, "description": "User not found"},
    },
    tags=["Users"],
)
async def get_user(user_id: int) -> UserResponse:
    ...

Documentation conventions:

  • Every endpoint has a summary (short) and optional description (detailed)
  • Document all non-200 responses with their schema
  • Group endpoints by tags matching the resource name
  • Use response_model for automatic response schema documentation

Examples

Designing a Products API Contract

Objective: Design the contract for a /v1/products CRUD endpoint with search and pagination.

Endpoints:

MethodPathDescriptionRequestResponseStatus
GET/v1/productsList productsQuery: cursor, limit, q, category, sortProductListResponse200
POST/v1/productsCreate productBody: ProductCreateProductResponse201
GET/v1/products/{id}Get productProductResponse200
PATCH/v1/products/{id}Update productBody: ProductPatchProductResponse200
DELETE/v1/products/{id}Delete product204

Schemas:

class ProductCreate(BaseModel):
    name: str = Field(min_length=1, max_length=200)
    description: str | None = None
    price_cents: int = Field(gt=0)
    category: str
    sku: str = Field(pattern=r"^[A-Z0-9-]+$")

class ProductPatch(BaseModel):
    name: str | None = None
    description: str | None = None
    price_cents: int | None = Field(default=None, gt=0)
    category: str | None = None

class ProductResponse(BaseModel):
    id: int
    name: str
    description: str | None
    price_cents: int
    category: str
    sku: str
    created_at: datetime
    updated_at: datetime

class ProductListResponse(BaseModel):
    items: list[ProductResponse]
    next_cursor: str | None
    has_more: bool

Search and filtering:

GET /v1/products?q=laptop&category=electronics&sort=-price_cents&limit=20

See references/endpoint-catalog-template.md for the full documentation template. See references/pydantic-schema-examples.md for additional schema examples.

Edge Cases

Bulk Operations

For operations on multiple resources at once:

POST /v1/users/bulk

Request:

{
  "items": [
    {"email": "a@example.com", "name": "Alice"},
    {"email": "b@example.com", "name": "Bob"}
  ]
}

Response (partial success — status 207):

{
  "results": [
    {"index": 0, "status": "created", "data": {...}},
    {"index": 1, "status": "error", "error": {"detail": "Email already exists", "code": "CONFLICT"}}
  ],
  "succeeded": 1,
  "failed": 1
}

Use HTTP 207 Multi-Status when individual items can succeed or fail independently.

File Upload Endpoints

File uploads use multipart/form-data, not JSON:

@router.post("/v1/files", response_model=FileResponse, status_code=201)
async def upload_file(
    file: UploadFile,
    description: str = Form(default=""),
) -> FileResponse:
    ...

Validate file size and MIME type before processing. Return 413 Payload Too Large for oversized files.

Long-Running Operations

For operations that cannot complete within a normal request timeout:

  1. Return 202 Accepted with a status URL:

    {"status_url": "/v1/jobs/abc123", "estimated_completion": "2024-01-15T10:30:00Z"}
    
  2. Client polls the status URL:

    GET /v1/jobs/abc123 → {"status": "processing", "progress": 0.65}
    GET /v1/jobs/abc123 → {"status": "completed", "result_url": "/v1/reports/xyz"}
    

Sub-Resource Design

When a resource logically belongs to a parent but nesting would exceed 2 levels, use a top-level resource with a filter:

# Instead of: GET /v1/users/{id}/orders/{oid}/items
# Use:        GET /v1/order-items?order_id=123

This keeps URLs flat while maintaining the relationship through filtering.

Output File

Write the API design to api-design.md at the project root:

# API Design: [Feature Name]

## Endpoints

| Method | URL | Description | Auth |
|--------|-----|-------------|------|
| GET | /v1/users | List users | Required |
| POST | /v1/users | Create user | Required |

## Request/Response Schemas

### UserCreate
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| email | string | Yes | Valid email |
| name | string | Yes | 1-100 chars |

### UserResponse
| Field | Type | Description |
|-------|------|-------------|
| id | uuid | User ID |
| email | string | User email |

## Error Codes

| Code | HTTP Status | Description |
|------|-------------|-------------|
| USER_NOT_FOUND | 404 | User does not exist |
| EMAIL_EXISTS | 409 | Email already registered |

## Next Steps
- Run `/task-decomposition` to create implementation tasks
- Run `/python-backend-expert` to implement endpoints

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

e2e-testing

No summary provided by upstream source.

Repository SourceNeeds Review
Security

code-review-security

No summary provided by upstream source.

Repository SourceNeeds Review
General

react-frontend-expert

No summary provided by upstream source.

Repository SourceNeeds Review
General

react-testing-patterns

No summary provided by upstream source.

Repository SourceNeeds Review