---
name: rest-api-design
description: "REST API design: HTTP methods, status codes, pagination, versioning, rate limiting, caching headers, OpenAPI documentation. Use when a REST surface needs designing or reviewing: methods, status codes, pagination, versioning, caching."
tags: [rest, api, http, openapi, pagination, backend]
version: "2025.1"
---

# REST API Design

## Core Principles

REST APIs should be resource-oriented, use standard HTTP methods, return consistent response
shapes, and be self-documenting. Follow established conventions to make APIs predictable
and easy to integrate with.

## URL Structure

```
# Resources are nouns (plural), not verbs
GET    /api/v1/users              # List users
GET    /api/v1/users/:id          # Get single user
POST   /api/v1/users              # Create user
PUT    /api/v1/users/:id          # Full update user
PATCH  /api/v1/users/:id          # Partial update user
DELETE /api/v1/users/:id          # Delete user

# Nested resources (one level deep max)
GET    /api/v1/users/:id/posts    # List user's posts
POST   /api/v1/users/:id/posts    # Create post for user

# Actions that don't map to CRUD (use verbs sparingly)
POST   /api/v1/users/:id/activate
POST   /api/v1/orders/:id/cancel
POST   /api/v1/reports/generate

# Filtering, sorting, searching via query params
GET    /api/v1/posts?status=published&author_id=123
GET    /api/v1/posts?sort=-published_at,title
GET    /api/v1/posts?q=search+term
GET    /api/v1/posts?fields=id,title,slug      # Sparse fieldsets
```

## HTTP Methods

| Method | Idempotent | Safe | Request Body | Use Case |
|--------|-----------|------|-------------|----------|
| GET | Yes | Yes | No | Retrieve resource(s) |
| POST | No | No | Yes | Create resource, trigger action |
| PUT | Yes | No | Yes | Full resource replacement |
| PATCH | No | No | Yes | Partial resource update |
| DELETE | Yes | No | Optional | Remove resource |
| HEAD | Yes | Yes | No | Check resource existence / headers |
| OPTIONS | Yes | Yes | No | CORS preflight / capabilities |

## HTTP Status Codes

```
# Success
200 OK               -  Successful GET, PUT, PATCH, DELETE
201 Created          -  Successful POST (include Location header)
204 No Content       -  Successful DELETE with no response body
206 Partial Content  -  Paginated list or range request

# Client Errors
400 Bad Request      -  Invalid syntax, validation error
401 Unauthorized     -  Missing or invalid authentication
403 Forbidden        -  Authenticated but not authorized
404 Not Found        -  Resource does not exist
405 Method Not Allowed  -  HTTP method not supported on this endpoint
409 Conflict         -  Duplicate resource, state conflict
422 Unprocessable Entity  -  Valid syntax but semantic errors
429 Too Many Requests  -  Rate limit exceeded

# Server Errors
500 Internal Server Error  -  Unexpected server failure
502 Bad Gateway      -  Upstream service failure
503 Service Unavailable  -  Temporary overload or maintenance
504 Gateway Timeout  -  Upstream timeout
```

## Response Envelope

```typescript
// Consistent response shape

// Success: single resource
{
  "data": {
    "id": "user_abc123",
    "type": "user",
    "attributes": {
      "name": "Alice Johnson",
      "email": "alice@example.com",
      "role": "admin",
      "created_at": "2025-01-15T10:30:00Z"
    }
  }
}

// Success: collection
{
  "data": [
    { "id": "post_1", "type": "post", "attributes": { ... } },
    { "id": "post_2", "type": "post", "attributes": { ... } }
  ],
  "meta": {
    "total": 150,
    "page": 1,
    "per_page": 20,
    "has_next": true
  }
}

// Error response
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "field": "email", "message": "Must be a valid email address" },
      { "field": "age", "message": "Must be between 0 and 150" }
    ],
    "request_id": "req_xyz789"
  }
}
```

## Pagination

### Cursor-Based (Recommended)

```typescript
// Request
// GET /api/v1/posts?limit=20&after=eyJpZCI6InBvc3RfMTAwIn0

// Response
{
  "data": [ ... ],
  "pagination": {
    "has_next": true,
    "has_prev": false,
    "next_cursor": "eyJpZCI6InBvc3RfMTIwIn0",
    "prev_cursor": null
  }
}

// Server implementation (Node.js / Express)
async function listPosts(req: Request, res: Response) {
  const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
  const cursor = req.query.after ? decodeCursor(req.query.after as string) : null;

  const posts = await db.post.findMany({
    where: cursor ? { id: { gt: cursor.id } } : undefined,
    orderBy: { id: 'asc' },
    take: limit + 1, // Fetch one extra to check has_next
  });

  const hasNext = posts.length > limit;
  if (hasNext) posts.pop();

  res.json({
    data: posts,
    pagination: {
      has_next: hasNext,
      next_cursor: hasNext ? encodeCursor({ id: posts[posts.length - 1].id }) : null,
    },
  });
}
```

### Offset-Based (Simple, for small datasets)

```typescript
// GET /api/v1/posts?page=3&per_page=20

{
  "data": [ ... ],
  "meta": {
    "page": 3,
    "per_page": 20,
    "total": 157,
    "total_pages": 8
  }
}
```

## Versioning

```
# URL versioning (most common, recommended)
GET /api/v1/users
GET /api/v2/users

# Header versioning
GET /api/users
Accept: application/vnd.myapp.v2+json

# Query parameter versioning (least preferred)
GET /api/users?version=2
```

### Version Evolution Strategy

```
1. Add new fields without version bump (non-breaking)
2. Deprecate fields: add "deprecated" flag, set sunset date
3. New version for breaking changes:
   - Removing fields
   - Renaming fields
   - Changing response structure
   - Changing authentication
4. Support N-1 versions minimum
5. Return Sunset and Deprecation headers:
   Deprecation: true
   Sunset: Sat, 01 Mar 2026 00:00:00 GMT
```

## Rate Limiting

```typescript
// Response headers
// X-RateLimit-Limit: 100           -  Max requests per window
// X-RateLimit-Remaining: 67        -  Remaining requests
// X-RateLimit-Reset: 1705312800    -  Unix timestamp when window resets
// Retry-After: 30                  -  Seconds to wait (on 429)

// Express middleware example
import rateLimit from 'express-rate-limit';

const apiLimiter = rateLimit({
  windowMs: 60 * 1000,  // 1 minute
  max: 100,             // 100 requests per window
  standardHeaders: true, // Send RateLimit-* headers
  legacyHeaders: false,
  message: {
    error: {
      code: 'RATE_LIMIT_EXCEEDED',
      message: 'Too many requests. Please try again later.',
    },
  },
});

app.use('/api/', apiLimiter);

// Different limits per tier
const premiumLimiter = rateLimit({ windowMs: 60_000, max: 1000 });
const freeLimiter = rateLimit({ windowMs: 60_000, max: 50 });
```

## Caching Headers

```typescript
// Immutable static assets
res.set('Cache-Control', 'public, max-age=31536000, immutable');

// Dynamic content with revalidation
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
res.set('ETag', `"${contentHash}"`);

// Private user data
res.set('Cache-Control', 'private, no-cache, no-store');

// ETag-based conditional requests
app.get('/api/v1/posts/:id', async (req, res) => {
  const post = await getPost(req.params.id);
  const etag = `"${hashContent(post)}"`;

  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }

  res.set('ETag', etag);
  res.set('Cache-Control', 'public, max-age=60');
  res.json({ data: post });
});
```

## OpenAPI / Swagger Documentation

```yaml
# openapi.yaml
openapi: 3.1.0
info:
  title: My API
  version: 1.0.0
  description: Production REST API

servers:
  - url: https://api.example.com/v1

paths:
  /users:
    get:
      summary: List users
      tags: [Users]
      parameters:
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
        - name: per_page
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
      responses:
        '200':
          description: List of users
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/User' }
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'

    post:
      summary: Create user
      tags: [Users]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateUserRequest' }
      responses:
        '201':
          description: User created
          headers:
            Location:
              schema: { type: string }
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/User' }
        '422':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

components:
  schemas:
    User:
      type: object
      required: [id, name, email, role, created_at]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        email: { type: string, format: email }
        role: { type: string, enum: [admin, user, viewer] }
        created_at: { type: string, format: date-time }
```

## Do's

- Use nouns for resources, HTTP methods for actions
- Return consistent response envelopes for all endpoints
- Use cursor-based pagination for large or frequently-changing datasets
- Include `request_id` in error responses for debugging
- Version your API from day one (URL-based: `/v1/`)
- Use proper HTTP status codes (not 200 for everything)
- Document every endpoint with OpenAPI/Swagger
- Include rate limit headers on all responses

## Don'ts

- Do not use verbs in URLs for standard CRUD (`/getUsers`, `/createUser`)
- Do not return 200 status with an error body
- Do not nest resources more than one level deep
- Do not return server stack traces in production error responses
- Do not use sequential integer IDs exposed in URLs (use UUIDs)
- Do not cache responses with sensitive user data
- Do not break backward compatibility without a version bump
- Do not skip input validation on any endpoint

## Troubleshooting

| Problem | Cause | Solution |
|---------|-------|----------|
| 401 vs 403 confusion | Mixing authentication and authorization | 401 = who are you? 403 = you can't do this |
| Slow list endpoints | Missing pagination or N+1 queries | Add cursor pagination, optimize DB queries |
| Inconsistent errors | No standard error format | Implement error envelope middleware |
| CORS errors | Missing headers on OPTIONS response | Configure CORS middleware with allowed origins |
| Stale cache | No cache invalidation strategy | Use ETag + conditional requests, short max-age |
| Breaking clients | Schema change without versioning | Add new version, deprecate old with Sunset header |
