## Resource Design

**URL structure -- nouns, plural, lowercase, kebab-case:**

```
GET    /api/v1/users
GET    /api/v1/users/:id
POST   /api/v1/users
PUT    /api/v1/users/:id
PATCH  /api/v1/users/:id
DELETE /api/v1/users/:id

# Sub-resources for relationships
GET    /api/v1/users/:id/orders
POST   /api/v1/users/:id/orders

# Actions that don't map to CRUD (use verbs sparingly)
POST   /api/v1/orders/:id/cancel
POST   /api/v1/auth/login
POST   /api/v1/auth/refresh
```

**Naming rules:**

```
// GOOD: kebab-case, plural, query params for filtering
/api/v1/team-members
/api/v1/orders?status=active
/api/v1/users/123/orders

// BAD: verbs in URL, singular, snake_case
/api/v1/getUsers
/api/v1/user
/api/v1/team_members
/api/v1/users/123/getOrders
```

## HTTP Methods and Status Codes

**Method semantics:**

| Method | Idempotent | Safe | Use For |
|--------|-----------|------|---------|
| GET | Yes | Yes | Retrieve resources |
| POST | No | No | Create resources, trigger actions |
| PUT | Yes | No | Full replacement of a resource |
| PATCH | No* | No | Partial update of a resource |
| DELETE | Yes | No | Remove a resource |

*PATCH can be made idempotent with proper implementation.

**Status code reference:**

```
# Success
200 OK                    -- GET, PUT, PATCH (with response body)
201 Created               -- POST (include Location header)
204 No Content            -- DELETE, PUT (no response body)

# Client Errors
400 Bad Request           -- Validation failure, malformed JSON
401 Unauthorized          -- Missing or invalid authentication
403 Forbidden             -- Authenticated but not authorized
404 Not Found             -- Resource doesn't exist
409 Conflict              -- Duplicate entry, state conflict
422 Unprocessable Entity  -- Semantically invalid (valid JSON, bad data)
429 Too Many Requests     -- Rate limit exceeded

# Server Errors
500 Internal Server Error -- Unexpected failure (never expose details)
502 Bad Gateway           -- Upstream service failed
503 Service Unavailable   -- Temporary overload, include Retry-After
```

**Status code usage:**

```
// BAD: 200 for everything
{ "status": 200, "success": false, "error": "Not found" }

// GOOD: use HTTP status codes semantically
HTTP/1.1 404 Not Found
{ "error": { "code": "not_found", "message": "User not found" } }

// BAD: 200 for created resources
// GOOD: 201 with Location header
HTTP/1.1 201 Created
Location: /api/v1/users/abc-123
```

## Response Envelope

**Single resource:**

```json
{
  "data": {
    "id": "abc-123",
    "email": "alice@example.com",
    "name": "Alice",
    "created_at": "2025-01-15T10:30:00Z"
  }
}
```

**Collection with pagination:**

```json
{
  "data": [
    { "id": "abc-123", "name": "Alice" },
    { "id": "def-456", "name": "Bob" }
  ],
  "meta": {
    "total": 142,
    "page": 1,
    "per_page": 20,
    "total_pages": 8
  },
  "links": {
    "self": "/api/v1/users?page=1&per_page=20",
    "next": "/api/v1/users?page=2&per_page=20",
    "last": "/api/v1/users?page=8&per_page=20"
  }
}
```

**Error response:**

```json
{
  "error": {
    "code": "validation_error",
    "message": "Request validation failed",
    "details": [
      { "field": "email", "message": "Must be a valid email address", "code": "invalid_format" },
      { "field": "age", "message": "Must be between 0 and 150", "code": "out_of_range" }
    ]
  }
}
```

**Envelope type definitions:**

```typescript
// Option A: Envelope with data wrapper (recommended for public APIs)
interface ApiResponse<T> {
  data: T;
  meta?: PaginationMeta;
  links?: PaginationLinks;
}

interface ApiError {
  error: {
    code: string;
    message: string;
    details?: FieldError[];
  };
}

// Option B: Flat response (simpler, common for internal APIs)
// Return the resource directly on success, error object on failure.
// Distinguish by HTTP status code.
```

## Pagination

**Offset-based (simple):**

```
GET /api/v1/users?page=2&per_page=20

SELECT * FROM users ORDER BY created_at DESC LIMIT 20 OFFSET 20;
```

Pros: easy to implement, supports "jump to page N".
Cons: slow on large offsets (OFFSET 100000), inconsistent with concurrent inserts.

**Cursor-based (scalable):**

```
GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20

SELECT * FROM users WHERE id > :cursor_id ORDER BY id ASC LIMIT 21;
-- fetch one extra to determine has_next
```

```json
{
  "data": [],
  "meta": { "has_next": true, "next_cursor": "eyJpZCI6MTQzfQ" }
}
```

Pros: consistent performance regardless of position, stable with concurrent inserts.
Cons: cannot jump to arbitrary page, cursor is opaque.

**Decision table:**

| Use Case | Pagination Type |
|----------|----------------|
| Admin dashboards, small datasets (<10K) | Offset |
| Infinite scroll, feeds, large datasets | Cursor |
| Public APIs | Cursor (default) with offset (optional) |
| Search results | Offset (users expect page numbers) |

## Filtering, Sorting, and Search

**Filtering:**

```
# Simple equality
GET /api/v1/orders?status=active&customer_id=abc-123

# Comparison operators (bracket notation)
GET /api/v1/products?price[gte]=10&price[lte]=100
GET /api/v1/orders?created_at[after]=2025-01-01

# Multiple values (comma-separated)
GET /api/v1/products?category=electronics,clothing

# Nested fields (dot notation)
GET /api/v1/orders?customer.country=US
```

**Sorting:**

```
# Single field (prefix - for descending)
GET /api/v1/products?sort=-created_at

# Multiple fields (comma-separated)
GET /api/v1/products?sort=-featured,price,-created_at
```

**Full-text search:**

```
GET /api/v1/products?q=wireless+headphones
```

**Sparse fieldsets:**

```
GET /api/v1/users?fields=id,name,email
GET /api/v1/orders?fields=id,total,status&include=customer.name
```

## Rate Limiting

**Response headers:**

```
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640000000

# When exceeded
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Try again in 60 seconds."
  }
}
```

**Rate limit tiers:**

| Tier | Limit | Window | Use Case |
|------|-------|--------|----------|
| Anonymous | 30/min | Per IP | Public endpoints |
| Authenticated | 100/min | Per user | Standard API access |
| Premium | 1000/min | Per API key | Paid API plans |
| Internal | 10000/min | Per service | Service-to-service |

## Versioning

**URL path versioning (recommended):**

```
/api/v1/users
/api/v2/users
```

Pros: explicit, easy to route, cacheable.
Cons: URL changes between versions.

**Header versioning:**

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

Pros: clean URLs.
Cons: harder to test, easy to forget.

**Versioning strategy:**

1. Start with `/api/v1/` -- don't version until you need to.
2. Maintain at most 2 active versions (current + previous).
3. Deprecation timeline:
   - Announce deprecation (6 months notice for public APIs).
   - Add `Sunset` header: `Sunset: Sat, 01 Jan 2026 00:00:00 GMT`.
   - Return `410 Gone` after sunset date.
4. Non-breaking changes don't need a new version:
   - Adding new fields to responses.
   - Adding new optional query parameters.
   - Adding new endpoints.
5. Breaking changes require a new version:
   - Removing or renaming fields.
   - Changing field types.
   - Changing URL structure.
   - Changing authentication method.

## Implementation Patterns

**TypeScript (Next.js API Route):**

```typescript
import { z } from "zod";
import { NextRequest, NextResponse } from "next/server";

const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
});

export async function POST(req: NextRequest) {
  const body = await req.json();
  const parsed = createUserSchema.safeParse(body);

  if (!parsed.success) {
    return NextResponse.json({
      error: {
        code: "validation_error",
        message: "Request validation failed",
        details: parsed.error.issues.map(i => ({
          field: i.path.join("."),
          message: i.message,
          code: i.code,
        })),
      },
    }, { status: 422 });
  }

  const user = await createUser(parsed.data);
  return NextResponse.json(
    { data: user },
    { status: 201, headers: { Location: `/api/v1/users/${user.id}` } },
  );
}
```

**Python (Django REST Framework):**

```python
from rest_framework import serializers, status
from rest_framework.response import Response
from rest_framework.views import APIView

class CreateUserSerializer(serializers.Serializer):
    email = serializers.EmailField()
    name = serializers.CharField(max_length=100)

class UserView(APIView):
    def post(self, request):
        serializer = CreateUserSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = UserService.create(**serializer.validated_data)
        return Response(
            {"data": UserSerializer(user).data},
            status=status.HTTP_201_CREATED,
            headers={"Location": f"/api/v1/users/{user.id}"},
        )
```

**Go (net/http):**

```go
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
        return
    }
    if err := req.Validate(); err != nil {
        writeError(w, http.StatusUnprocessableEntity, "validation_error", err.Error())
        return
    }
    user, err := h.service.Create(r.Context(), req)
    if err != nil {
        switch {
        case errors.Is(err, domain.ErrEmailTaken):
            writeError(w, http.StatusConflict, "email_taken", "Email already registered")
        default:
            writeError(w, http.StatusInternalServerError, "internal_error", "Internal error")
        }
        return
    }
    w.Header().Set("Location", fmt.Sprintf("/api/v1/users/%s", user.ID))
    writeJSON(w, http.StatusCreated, map[string]any{"data": user})
}
```

## API Design Checklist

Before shipping a new endpoint:

- Resource URL follows naming conventions (plural, kebab-case, no verbs)
- Correct HTTP method used (GET for reads, POST for creates, etc.)
- Appropriate status codes returned (not 200 for everything)
- Input validated with schema (Zod, Pydantic, Bean Validation)
- Error responses follow standard format with codes and messages
- Pagination implemented for list endpoints (cursor or offset)
- Authentication required (or explicitly marked as public)
- Authorization checked (user can only access their own resources)
- Rate limiting configured
- Response does not leak internal details (stack traces, SQL errors)
- Consistent naming with existing endpoints (camelCase vs snake_case)
- Documented (OpenAPI/Swagger spec updated)

## Common Anti-Patterns

**Status code misuse:**

- Returning 200 for every response with a `success` boolean -- use semantic HTTP status codes
- Returning 500 for validation errors -- use 400 or 422 with field-level details
- Returning 200 for created resources -- use 201 with Location header

**URL design:**

- Verbs in URLs (`/getUsers`, `/deleteOrder`) -- HTTP method conveys the verb
- Singular resource names (`/user`) -- always use plural (`/users`)
- snake_case in URLs (`/team_members`) -- use kebab-case (`/team-members`)
- Deeply nested resources beyond 2 levels (`/a/:id/b/:id/c/:id/d`) -- flatten or use query params

**Response design:**

- Inconsistent envelope shapes across endpoints
- Missing pagination on list endpoints that can grow
- Exposing internal IDs, stack traces, or SQL errors in responses
- No versioning strategy until the first breaking change is needed

**Authentication:**

- API keys in query strings (visible in logs) -- use headers
- No rate limiting on authentication endpoints (brute force risk)
- Missing authorization checks after authentication (authn != authz)
