# API Design

> API design rules for modern frontend - REST vs GraphQL, naming, HTTP methods, status codes, versioning, auth, rate limiting, pagination, error handling, CORS, HTTP caching, OpenAPI, client architecture

- Skill: `14bryanespinoza/api-design` (Agent Skill)
- Install (CLI): `npx skillmds@latest add 14bryanespinoza/api-design`
- Raw SKILL.md: https://api.skillmd.com/api/skills/14bryanespinoza/api-design/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: 14BryanEspinoza (https://skillmd.com/u/14bryanespinoza)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/14bryanespinoza/api-design

---


# API Design — Rules

---

## 1. Philosophy

1. **REST by default** — Predictable, cacheable, tooling-friendly. GraphQL only when complexity justifies it.
2. **Design for consumers** — Frontend-driven contracts. Version explicitly.
3. **Errors are first-class** — Consistent format, actionable messages, traceable.
4. **Observability built-in** — Request IDs, structured logs, metrics.
5. **Security by default** — Auth, rate limiting, validation on every endpoint.

---

## 2. REST vs GraphQL

| Factor             | REST                                            | GraphQL                                             |
| ------------------ | ----------------------------------------------- | --------------------------------------------------- |
| **Use when**       | Simple CRUD, caching critical, team familiarity | Complex relations, multiple clients, evolving needs |
| **Caching**        | HTTP native (ETag, Cache-Control)               | Client-side (Apollo, Relay)                         |
| **Over-fetching**  | Common                                          | None                                                |
| **Under-fetching** | Multiple requests                               | Single request                                      |
| **Learning curve** | Low                                             | Medium                                              |
| **Tooling**        | Universal                                       | Specialized                                         |

**Default: REST** — GraphQL only with team consensus and documented
justification.

---

## 3. Resource Naming

### Conventions

```text
# Collections (plural, kebab-case)
GET    /api/users
POST   /api/users
GET    /api/users/{id}
PATCH  /api/users/{id}
DELETE /api/users/{id}

# Nested resources
GET    /api/users/{id}/posts
POST   /api/users/{id}/posts

# Actions (avoid verbs in URL)
POST   /api/users/{id}/activate   # → PATCH /api/users/{id} { status: "active" }
POST   /api/payments/{id}/refund  # → POST /api/refunds { paymentId }
```

### Rules

- **Plural nouns** — `/users` not `/user`
- **Kebab-case** — `/user-profiles` not `/userProfiles`
- **No verbs** — use HTTP methods
- **Hierarchy max 2 levels** — `/users/{id}/posts/{id}/comments` OK, deeper → flatten

---

## 4. HTTP Methods

| Method    | Semantics      | Idempotent | Body | Response |
| --------- | -------------- | ---------- | ---- | -------- |
| `GET`     | Retrieve       | ✅         | No   | 200, 404 |
| `POST`    | Create         | ❌         | Yes  | 201, 400 |
| `PUT`     | Replace (full) | ✅         | Yes  | 200, 404 |
| `PATCH`   | Partial update | ✅         | Yes  | 200, 404 |
| `DELETE`  | Delete         | ✅         | No   | 204, 404 |
| `HEAD`    | Metadata only  | ✅         | No   | 200, 404 |
| `OPTIONS` | Capabilities   | ✅         | No   | 200      |

### Rules HTTP Methods

- **`POST` for create** — returns 201 + `Location` header
- **`PATCH` for partial** — `PUT` replaces entire resource
- **`DELETE` returns 204** — no body
- **Idempotency keys** for `POST`/`PATCH` — `Idempotency-Key` header

---

## 5. Status Codes

| Code  | Use Case                                         |
| ----- | ------------------------------------------------ |
| `200` | Success (GET, PUT, PATCH)                        |
| `201` | Created (POST) — include `Location`              |
| `204` | No Content (DELETE, successful no-body)          |
| `400` | Bad Request — validation errors                  |
| `401` | Unauthorized — missing/invalid auth              |
| `403` | Forbidden — valid auth, insufficient permissions |
| `404` | Not Found                                        |
| `409` | Conflict — version mismatch, duplicate           |
| `422` | Unprocessable Entity — semantic errors           |
| `429` | Too Many Requests — rate limited                 |
| `500` | Server Error — log with request ID               |
| `503` | Service Unavailable — maintenance, overload      |

### Rules Status

- **Never return 200 with error body** — use appropriate 4xx/5xx
- **401 vs 403** — 401 = "who are you?", 403 = "you can't do this"
- **Include `requestId`** in all error responses for tracing

---

## 6. Error Handling

### Format (RFC 9457 / Problem Details)

```json
{
  "type": "https://api.example.com/errors/validation-failed",
  "title": "Validation failed",
  "status": 422,
  "detail": "Email already registered",
  "instance": "/api/users",
  "requestId": "req_abc123",
  "errors": [
    {
      "field": "email",
      "code": "already_exists",
      "message": "Email already registered"
    }
  ]
}
```

### Required fields

| Field       | Required | Description               |
| ----------- | -------- | ------------------------- |
| `type`      | ✅       | URI for error category    |
| `title`     | ✅       | Human-readable summary    |
| `status`    | ✅       | HTTP status code          |
| `detail`    | ✅       | Specific explanation      |
| `instance`  | ✅       | Request path              |
| `requestId` | ✅       | Trace ID                  |
| `errors`    | ⚠️       | Field-level (for 400/422) |

### Rules Error Handling

- **Consistent format** — all errors follow this schema
- **No stack traces** in production
- **Localized `title`** — accept `Accept-Language` header
- **`requestId` in response header** — `X-Request-Id`

---

## 7. Pagination

### Cursor-based (preferred)

```http
GET /api/users?limit=20&after=eyJpZCI6MTAwfQ
```

```json
{
  "data": [...],
  "pagination": {
    "limit": 20,
    "nextCursor": "eyJpZCI6MTIwfQ",
    "hasMore": true
  }
}
```

### Offset (legacy only)

```http
GET /api/users?page=2&limit=20
```

```json
{
  "data": [...],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 150,
    "totalPages": 8
  }
}
```

### Rules Pagination

- **Cursor-based default** — stable, performant, no skipped items
- **Max limit 100** — enforce server-side
- **`hasMore` boolean** — client doesn't calculate
- **No negative offsets**

---

## 8. Filtering, Sorting, Searching

### Filtering

```http
GET /api/users?filter[status]=active&filter[role]=admin
GET /api/users?filter[createdAt][$gte]=2024-01-01
```

### Sorting

```http
GET /api/users?sort=-createdAt,email
```

### Searching

```http
GET /api/users?q=john@example.com
```

### Rules Filtering

- **`filter[field][operator]`** — explicit operators (`$eq`, `$gt`,
  `$lt`, `$in`, `$like`)
- **`sort` comma-separated** — `-` prefix for desc
- **`q` for full-text** — single search parameter
- **Whitelist allowed fields** — reject unknown

---

## 9. Versioning

### URL versioning (preferred)

```http
GET /api/v1/users
GET /api/v2/users
```

### Header versioning (alternative)

```http
GET /api/users
Accept: application/vnd.example.v2+json
```

### Rules Versioning

- **Major versions in URL** — `/v1/`, `/v2/`
- **Minor versions backward compatible** — additive only
- **Deprecation header** — `Deprecation: true`, `Sunset: Sat, 01 Jan 2025 00:00:00 GMT`
- **Support 2 versions max** — deprecate after 12 months

---

## 10. Authentication

> **Auth patterns**: see `auth` skill.

### Summary

| Method             | Use Case                   |
| ------------------ | -------------------------- |
| **Bearer (JWT)**   | Stateless, microservices   |
| **Session/Cookie** | SSR, traditional apps      |
| **API Key**        | Server-to-server, webhooks |

### Rules Authentication

- **`Authorization: Bearer <token>`** for JWT
- **`Cookie`** for sessions — `HttpOnly`, `Secure`, `SameSite=Lax`
- **401 with `WWW-Authenticate`** — `Bearer realm="api"`

---

## 11. Rate Limiting

### Headers

```http
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1704067200
Retry-After: 60
```

### Response (429)

```json
{
  "type": "https://api.example.com/errors/rate-limited",
  "title": "Rate limit exceeded",
  "status": 429,
  "detail": "Limit 100 requests per minute. Retry after 60 seconds.",
  "retryAfter": 60
}
```

### Rules Rate Limiting

- **Per-IP + per-user** — both layers
- **Standard headers** — `X-RateLimit-*`, `Retry-After`
- **Different limits** — auth endpoints stricter (5/min), read generous (100/min)
- **Return 429** with `Retry-After` seconds

---

## 12. CORS

```http
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, Idempotency-Key
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
```

### Rules CORS

- **Exact origin** — no `*` with credentials
- **Preflight caching** — `Max-Age` 24h
- **Allow only needed headers** — `Authorization`, `Content-Type`,
  `Idempotency-Key`

---

## 13. HTTP Caching

### Response headers

```http
# Immutable (hashed assets)
Cache-Control: public, max-age=31536000, immutable

# Private (user-specific)
Cache-Control: private, max-age=0, must-revalidate

# Public with revalidation
Cache-Control: public, max-age=60, stale-while-revalidate=300
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
```

### Rules HTTP Caching

- **`ETag` + `If-None-Match`** — prefer over `Last-Modified`
- **`stale-while-revalidate`** — serve stale while revalidating
- **No cache for auth/mutations** — `private, no-store`

---

## 14. Content Negotiation

```http
Accept: application/json
Accept: application/vnd.example.v2+json
Accept-Language: en-US,en;q=0.9
```

### Rules Content Negotiation

- **Default: `application/json`**
- **Version via `Accept` header** — `application/vnd.example.v2+json`
- **`Accept-Language`** for localized errors
- **406 Not Acceptable** if unsupported

---

## 15. OpenAPI

### Minimal config

```yaml
# openapi.yaml
openapi: 3.1.0
info:
  title: Example API
  version: 1.0.0
servers:
  - url: https://api.example.com/v1
paths:
  /users:
    get:
      summary: List users
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/after"
      responses:
        "200":
          description: Success
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserList"
components:
  schemas:
    UserList:
      type: object
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/User" }
        pagination:
          $ref: "#/components/schemas/Pagination"
  parameters:
    limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
    after:
      name: after
      in: query
      schema: { type: string }
```

### Rules OpenAPI

- **Contract-first** — generate client/server from spec
- **Single source** — `openapi.yaml` in repo
- **CI validates** — spec lint + breaking change detection
- **Generate client** — `openapi-typescript`, `orval`

---

## 16. Dates and IDs

### Dates

```json
{ "createdAt": "2024-01-15T10:30:00Z" }
```

- **ISO 8601 UTC** — always `Z` suffix
- **No Unix timestamps** in JSON

### IDs

```json
{ "id": "usr_abc123def456" }
```

- **Prefixed ULIDs** — `usr_`, `org_`, `pay_` (sortable, readable)
- **No raw UUIDs** — no prefix, not sortable

---

## 17. Methodology

Before using ANY API pattern not documented in this skill:

1. **MCP Context7** (priority): `context7_resolve-library-id` + `context7_query-docs` for OpenAPI, REST patterns.
2. **Official docs**: RESTful API standards, RFC 9457, HTTP specs.
3. **Project config**: `openapi.yaml`, API gateway config — verify against actual setup.
4. **HARD RULE**: If not in this skill AND cannot be verified against 2 authoritative sources → DO NOT USE IT. Document as assumption or risk in report to orchestrator.

---

## 18. Prohibitions

- ❌ Do not use verbs in URLs (`/getUsers`, `/createUser`)
- ❌ Do not return 200 with error payload
- ❌ Do not use `PUT` for partial updates
- ❌ Do not use offset pagination for large datasets
- ❌ Do not expose internal IDs (DB PKs) — use prefixed ULIDs
- ❌ Do not skip `requestId` in errors
- ❌ Do not use `*` in CORS with credentials
- ❌ Do not version in query string (`?v=2`)
- ❌ Do not break backward compatibility without deprecation period

---

## 19. References

> **Note:** For JavaScript conventions (fetch, client), see [JavaScript](../javascript/SKILL.md)
> **Note:** For TypeScript types, see [TypeScript](../typescript/SKILL.md)
> **Note:** For Auth patterns, see [Auth](../auth/SKILL.md)
> **Note:** For Security (rate limiting, CORS), see [Security](../security/SKILL.md)

---

Last updated: 2026-08

