design-api · git:20260923.68c095e · 2026-09-23 · sha256 03bc348de6e2652b

design-api git:20260923.68c095eA

Immutable. This exact content never changes and is served at /api/v1/blob/03bc348de6e2652b.

---
name: design-api
description: >
  Design REST and GraphQL APIs: naming, versioning, error shapes, auth. Use
  when "design an API", "create endpoints", "structure my API responses",
  "plan API architecture", "REST vs GraphQL", or "API contract".
license: MIT
effort: high
---

# API Design Skill

**Degree of freedom: MIXED.** Resource model, error shape, and REST vs
GraphQL `[HIGH freedom]`; pre-design docs/schema/grep
`[LOW freedom — run exactly]`.

Design clean, consistent, and developer-friendly APIs.

## How to reason

1. **Survey** — existing docs, schema, and similar endpoints
2. **Model** — resources, relations, REST vs GraphQL
3. **Contract** — paths, statuses, error shape, auth, pagination
4. **Check** — naming matches this repo; no duplicate endpoint

## Worked example

> **Survey:** `orders` has `user_id`; no `GET /users/:id/orders`; clients use `useQuery`.
> **Model:** order is a nested user resource, not a `/getUserOrders` RPC.
> **Contract:** `GET /users/:id/orders` → `{ data, meta }`; shared `{ error: { code, message, details } }`.
> **Check:** plural kebab-case; list paginated; 401/404/422 only from the status table.

## Self-critique before reporting

- **Pre-design stated** — docs, schema, and similar endpoints were checked out loud
- **One error shape** — every failure uses `{ error: { code, message, details } }`
- **Lists paginate** — no unbounded `GET /resources`
- **Right owner** — live 4xx/5xx repro → `debug-fe-be-integration`; product scope still fuzzy → `design-prd`

## Pre-design checks  [LOW freedom — run exactly]

**Before designing any API:**

### 1. Check Existing API Documentation
- The running backend's docs route (`/api-docs`, `/docs`, `/swagger`, `/openapi.json`)
- The repo's API README or naming-conventions doc (grep `naming` under `docs/` and `src/api/`)

### 2. Verify Database Schema
Use Supabase MCP to understand existing data structure:
```sql
-- Check table schema
SELECT column_name, data_type, is_nullable
FROM information_schema.columns WHERE table_name = 'your_table';

-- Check enum values
SELECT enum_range(NULL::your_enum_name);

-- Check foreign keys
SELECT tc.constraint_name, kcu.column_name, ccu.table_name AS foreign_table
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name
WHERE tc.table_name = 'your_table' AND tc.constraint_type = 'FOREIGN KEY';
```

### 3. Check for Existing Endpoints
Use `Grep` to search for similar endpoints already implemented:
```
Grep: "router.get|router.post" to find existing route patterns
Grep: "useQuery|useMutation" to find existing frontend integrations
```

### 4. Verification Statement (REQUIRED)
Before designing, state:
```
"Pre-design check:
- Existing API docs reviewed: [YES/NO]
- Database schema verified: [tables/enums checked]
- Similar endpoints found: [list or none]
- Naming conventions confirmed: [YES/NO]"
```

---

## REST API Design  [HIGH freedom]

### URL Structure

```
GET /resources # List
GET /resources/:id # Get one
POST /resources # Create
PUT /resources/:id # Replace
PATCH /resources/:id # Update
DELETE /resources/:id # Delete
```

### Naming Conventions

| Do | Don't |
|----|-------|
| `/users` | `/getUsers`, `/user-list` |
| `/users/:id` | `/user/:id`, `/users/get/:id` |
| `/users/:id/orders` | `/getUserOrders` |
| Plural nouns | Verbs, singular |
| kebab-case | camelCase, snake_case |

### Examples

```
GET /users # List users
GET /users/123 # Get user 123
GET /users/123/orders # User's orders
GET /users/123/orders/456 # Specific order
POST /users/123/orders # Create order for user
```

---

## Request/Response Format  [LOW freedom — run this shape]

### Request Body

```json
{
 "name": "John Doe",
 "email": "john@example.com",
 "role": "admin"
}
```

### Successful Response

```json
{
 "data": {
 "id": "123",
 "name": "John Doe",
 "email": "john@example.com",
 "createdAt": "2024-01-15T10:30:00Z"
 }
}
```

### List Response (with pagination)

```json
{
 "data": [
 { "id": "1", "name": "John" },
 { "id": "2", "name": "Jane" }
 ],
 "meta": {
 "total": 100,
 "page": 1,
 "perPage": 20,
 "totalPages": 5
 }
}
```

### Error Response

```json
{
 "error": {
 "code": "VALIDATION_ERROR",
 "message": "Invalid input data",
 "details": [
 { "field": "email", "message": "Invalid email format" },
 { "field": "name", "message": "Name is required" }
 ]
 }
}
```

---

## HTTP Status Codes  [LOW freedom — use this table]

### Success (2xx)

| Code | When to Use |
|------|-------------|
| `200 OK` | GET, PUT, PATCH success |
| `201 Created` | POST created new resource |
| `204 No Content` | DELETE success, no body |

### Client Errors (4xx)

| Code | When to Use |
|------|-------------|
| `400 Bad Request` | Invalid request body |
| `401 Unauthorized` | Not authenticated |
| `403 Forbidden` | Authenticated but not allowed |
| `404 Not Found` | Resource doesn't exist |
| `409 Conflict` | Resource conflict (duplicate) |
| `422 Unprocessable` | Validation failed |
| `429 Too Many` | Rate limited |

### Server Errors (5xx)

| Code | When to Use |
|------|-------------|
| `500 Internal Error` | Unexpected server error |
| `502 Bad Gateway` | Upstream service failed |
| `503 Unavailable` | Service temporarily down |

---

## Query Parameters

### Filtering

```
GET /users?role=admin
GET /users?role=admin&status=active
GET /orders?createdAfter=2024-01-01
```

### Sorting

```
GET /users?sort=name # Ascending
GET /users?sort=-createdAt # Descending (prefix with -)
GET /users?sort=role,-name # Multiple fields
```

### Pagination

```
GET /users?page=2&perPage=20
GET /users?offset=40&limit=20
GET /users?cursor=abc123 # Cursor-based
```

### Field Selection

```
GET /users?fields=id,name,email
GET /users?include=orders,profile
```

---

## Versioning

### URL Path (Recommended)

```
GET /v1/users
GET /v2/users
```

### Header

```
GET /users
Accept: application/vnd.api+json;version=2
```

---

## Authentication

### Bearer Token

```
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
```

### API Key

```
X-API-Key: your-api-key
# or
?apiKey=your-api-key
```

---

## Common Patterns

### Bulk Operations

```
POST /users/bulk
{
 "create": [{ "name": "John" }, { "name": "Jane" }],
 "update": [{ "id": "1", "name": "Updated" }],
 "delete": ["2", "3"]
}
```

### Search

```
POST /users/search
{
 "query": "john",
 "filters": { "role": "admin" },
 "sort": { "field": "name", "order": "asc" }
}
```

### Actions (non-CRUD)

```
POST /orders/123/cancel
POST /users/123/verify-email
POST /payments/123/refund
```

---

## API Design Checklist  [LOW freedom — do not skip]

### Consistency
- [ ] Consistent naming conventions
- [ ] Consistent response format
- [ ] Consistent error format
- [ ] Consistent pagination

### Usability
- [ ] Intuitive URLs
- [ ] Clear documentation
- [ ] Meaningful error messages
- [ ] Sensible defaults

### Security
- [ ] Authentication required
- [ ] Authorization checked
- [ ] Input validation
- [ ] Rate limiting

### Performance
- [ ] Pagination for lists
- [ ] Field selection available
- [ ] Efficient queries
- [ ] Caching headers

---

## Documentation Template

```markdown
## Create User

Create a new user account.

**Endpoint:** `POST /users`

**Authentication:** Required (Bearer token)

**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| name | string | Yes | User's full name |
| email | string | Yes | Valid email address |
| role | string | No | User role (default: "user") |

**Response:** `201 Created`
\`\`\`json
{
 "data": {
 "id": "123",
 "name": "John Doe",
 "email": "john@example.com",
 "role": "user",
 "createdAt": "2024-01-15T10:30:00Z"
 }
}
\`\`\`

**Errors:**
- `400` - Invalid request body
- `409` - Email already exists
- `422` - Validation failed
```