api-design-patterns

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…

hieutrtr/ai1-skills61 installsMITSynced Aug 26

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI

Agent Skills format with YAML frontmatter. Claude Code reads it as-is.

---
name: "api-design-patterns"
description: "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…"
license: "MIT"
---

# 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

| Good | Bad | Reason |
|------|-----|--------|
| `GET /v1/users` | `GET /v1/getUsers` | No verbs — HTTP method implies action |
| `POST /v1/users` | `POST /v1/user/create` | POST to collection = create |
| `GET /v1/order-items` | `GET /v1/orderItems` | Kebab-case, not camelCase |
| `GET /v1/users/{id}/orders` | `GET /v1/users/{id}/orders/{oid}/items` | Max 2 nesting levels |
| `POST /v1/orders/{id}/actions/cancel` | `POST /v1/cancelOrder/{id}` | Action sub-resource for non-CRUD |

### HTTP Method Semantics

| Method | Purpose | Request Body | Success Status | Idempotent |
|--------|---------|-------------|----------------|------------|
| `GET` | Retrieve resource(s) | None | `200 OK` | Yes |
| `POST` | Create new resource | Required | `201 Created` | No |
| `PUT` | Full replace | Required (full) | `200 OK` | Yes |
| `PATCH` | Partial update | Required (partial) | `200 OK` | No* |
| `DELETE` | Remove resource | None | `204 No Content` | Yes |

*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:

| Pattern | Purpose | Fields |
|---------|---------|--------|
| `{Resource}Create` | POST request body | Writable fields, no id, no timestamps |
| `{Resource}Update` | PUT request body | All writable fields required |
| `{Resource}Patch` | PATCH request body | All fields Optional |
| `{Resource}Response` | Single resource response | All fields including id, timestamps |
| `{Resource}ListResponse` | Paginated list response | items + pagination metadata |
| `{Resource}Filter` | Query parameters | Optional 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
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `cursor` | `str \| None` | `None` | Opaque cursor from previous response |
| `limit` | `int` | `20` | Items per page (max 100) |

**Response format:**
```json
{
  "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).

```json
{
  "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:

```json
{
  "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 Status | When to Use | Example `code` |
|-------------|-------------|----------------|
| `400` | Malformed request | `BAD_REQUEST` |
| `401` | Missing or invalid authentication | `UNAUTHORIZED` |
| `403` | Authenticated but not authorized | `FORBIDDEN` |
| `404` | Resource not found | `NOT_FOUND` |
| `409` | Conflict (duplicate, version mismatch) | `CONFLICT` |
| `422` | Validation error (Pydantic) | `VALIDATION_ERROR` |
| `429` | Rate limit exceeded | `RATE_LIMITED` |
| `500` | Unexpected server error | `INTERNAL_ERROR` |

**Error schema (Pydantic v2):**
```python
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:

```python
@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:**

| Method | Path | Description | Request | Response | Status |
|--------|------|-------------|---------|----------|--------|
| GET | `/v1/products` | List products | Query: cursor, limit, q, category, sort | ProductListResponse | 200 |
| POST | `/v1/products` | Create product | Body: ProductCreate | ProductResponse | 201 |
| GET | `/v1/products/{id}` | Get product | — | ProductResponse | 200 |
| PATCH | `/v1/products/{id}` | Update product | Body: ProductPatch | ProductResponse | 200 |
| DELETE | `/v1/products/{id}` | Delete product | — | — | 204 |

**Schemas:**
```python
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:**
```json
{
  "items": [
    {"email": "a@example.com", "name": "Alice"},
    {"email": "b@example.com", "name": "Bob"}
  ]
}
```

**Response (partial success — status 207):**
```json
{
  "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:

```python
@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:
   ```json
   {"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:

```markdown
# 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
```

More General & Other skills

← All General & Other skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY