REST API Design¶
What is it: REST (Representational State Transfer) is an architectural style for APIs. It uses HTTP methods as the actions and URLs as the resources. Resources are nouns (not verbs).
Core constraints:
- Stateless: no session on the server; each request is self-contained
- Resource-based URLs: /users/123 not /getUser?id=123
- HTTP method semantics: GET to read, POST to create, PUT/PATCH to update, DELETE to remove
- Uniform interface: consistent behavior across all resources
URL design:
Use plural nouns for collections: /users, /orders. Use sub-resources for relationships: /users/123/orders (orders belonging to user 123). Avoid verbs in URLs: /users/123/activate is acceptable for non-CRUD operations but prefer /users/123/status with PATCH.
Status codes:
- 200 OK: success with body
- 201 Created: resource created (include Location: /users/456 header)
- 204 No Content: success with no body (DELETE)
- 400 Bad Request: client sent invalid data (validation error)
- 401 Unauthorized: not authenticated (no token or invalid token)
- 403 Forbidden: authenticated but not authorized (insufficient permissions)
- 404 Not Found: resource does not exist
- 409 Conflict: state conflict (duplicate email, optimistic lock failure)
- 422 Unprocessable Entity: semantic validation failure (valid JSON but business rule violated)
- 429 Too Many Requests: rate limit exceeded
- 500 Internal Server Error: unexpected server error
PUT vs PATCH:
PUT replaces the entire resource: you must send all fields. If you omit email, it is cleared. Safer for full object replacement. PATCH sends only the changed fields — more efficient for large objects with one field changing. PATCH is preferred in practice because clients rarely want to clear unmentioned fields.
Versioning:
URL versioning (/v1/users) is most common — explicit, easy to route in reverse proxies, easy to test. Header versioning (Accept: application/vnd.myapi.v1+json) keeps URLs clean but is harder to test in browsers. Bump the version when making breaking changes (removing fields, changing types, changing semantics). Maintain old versions for a deprecation window.
Designing a user service API:
GET /users → list users (with pagination: ?page=1&limit=20 or ?cursor=xxx)
POST /users → create user (body: {name, email, password})
GET /users/:id → get user by ID
PUT /users/:id → replace user
PATCH /users/:id → partial update
DELETE /users/:id → delete user
GET /users/:id/orders → list user's orders
POST /users/:id/orders → create order for user
Real-world usage: Proxel's internal API uses REST for job submission and status polling. gRPC-wallet uses gRPC (not REST) for internal service calls — binary, streaming, strongly typed — but exposes a REST gateway for external clients.
Common pitfall¶
Returning 200 OK for every response and encoding the real result in
the response body ({"success": false, "error": "not found"}) throws
away everything HTTP status codes exist to communicate — caches,
proxies, monitoring tools, and generic HTTP clients all key off the
status code, not the body's shape, which they don't know how to parse
generically. A 404 in the body with a 200 status looks like a
successful request to anything that isn't specifically your own
frontend's error-handling code, including uptime monitors and CDN
caching rules that would otherwise correctly treat a 4xx/5xx
differently from a real success.