Why this matters
Well-designed APIs make backend systems easy to use, scale, and maintain. As a Backend Engineer you will:
- Design endpoints for new features (e.g., user onboarding, orders, payments).
- Keep existing clients working when evolving your API.
- Handle errors predictably and securely.
- Ensure performance with pagination, filtering, caching, and rate limits.
Concept explained simply
An API is a contract between your service and its clients. Good design makes that contract obvious, consistent, and hard to misuse.
Mental model
Think of your API like a public library:
- Resources: books, authors, patrons. In APIs: users, orders, products.
- Uniform actions: check out, return. In APIs: standard HTTP methods (GET, POST, PATCH, DELETE).
- Clear rules: opening hours, fines. In APIs: authentication, rate limits, error codes.
Deep dive: REST vs RPC vs GraphQL
REST: Resource-centric, uses HTTP methods and status codes. Great for broad compatibility and caching.
RPC: Action-centric, often simpler for internal services, but can become ad hoc and harder to cache.
GraphQL: Client specifies fields; reduces over/under-fetching. Requires careful schema and caching strategy.
Pick based on use case, team expertise, and client needs. This lesson focuses on pragmatic REST-style JSON APIs.
Core principles
- Clarity: Predictable paths (/users, /orders/{id}).
- Consistency: Naming, casing (prefer snake_case or camelCase; be consistent), plural nouns for collections.
- Use HTTP semantics: GET (read), POST (create/action), PATCH (partial update), PUT (replace), DELETE (remove).
- Status codes: 2xx success, 4xx client errors, 5xx server errors. Be specific (201 Created, 400 Bad Request, 404 Not Found, 409 Conflict, 422 Unprocessable Entity).
- Idempotency: Safe retries for operations (GET, PUT, DELETE are idempotent; POST can be made idempotent with a key).
- Pagination & filtering: Avoid returning massive lists. Use query params (?page, ?limit, ?cursor) and filters (?status=active).
- Versioning: Keep clients working. Use URI (/v1/…) or header-based versioning. Avoid breaking changes.
- Error design: Structured error responses with code, message, and details.
- Security: Authentication (tokens), authorization (roles/scopes), input validation, rate limits, no sensitive data leakage.
- Observability: Correlation/request IDs, consistent logs, and standard headers to trace requests.
Worked examples
Example 1: Basic resource design
Goal: CRUD for products.
Design
{
Path: GET /v1/products -> list products (with pagination)
POST /v1/products -> create product
GET /v1/products/{product_id} -> get product
PATCH /v1/products/{product_id} -> update part of product
DELETE /v1/products/{product_id} -> delete product
Pagination: GET /v1/products?page=2&limit=25
Filtering: GET /v1/products?category=books&min_price=10
Status: 201 Created on POST with Location: /v1/products/123
}
Example 2: Idempotent POST (create order)
Problem: Client may retry due to network issues; duplicate orders must be avoided.
Design
{
Client sends: POST /v1/orders
Headers: Idempotency-Key: 7e5c-abc-111
Body: { "items": [{"sku":"A1","qty":2}], "payment_token":"tok_x" }
Server behavior:
- If new key: create order once -> 201 Created, Location: /v1/orders/9001
- If duplicate key: return the same result as first attempt (200/201), no new charge
}
Example 3: Consistent error responses
Design
{
On validation error (422):
Status: 422 Unprocessable Entity
Body: {
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid fields",
"details": [
{"field": "email", "issue": "must be a valid email"},
{"field": "age", "issue": "must be >= 18"}
],
"request_id": "req-5f2a"
}
}
}
Example 4: Pagination strategy
Offset vs cursor
{
Offset pagination:
GET /v1/users?page=3&limit=50
Pros: simple; Cons: unstable with inserts/deletes at scale.
Cursor pagination:
GET /v1/users?cursor=eyJpZCI6IjEyMyJ9&limit=50
Response includes next_cursor; stable for large datasets.
}
Example 5: Versioning without breaking clients
Design
{
Current: GET /v1/invoices/{id}
We need to add "due_date". Adding fields is backward-compatible.
Breaking change (rename field) requires new version:
- Keep /v1 as-is
- Introduce /v2/invoices/{id} with new field name
- Mark old field in /v1 as deprecated in docs and headers: Deprecation: true
}
Step-by-step design (use for new endpoints)
- Define resources and relationships (nouns). Example: users, orders, order_items.
- Map operations to HTTP methods (verbs). Default to GET/POST/PATCH/DELETE.
- Shape request/response JSON. Decide required/optional fields. Add examples.
- Choose pagination and filtering strategy.
- Decide authentication/authorization for each route.
- Design error model and codes.
- Make risky operations idempotent and safe to retry.
- Assign status codes and headers (Location, ETag, RateLimit-*).
- Plan versioning and deprecation strategy.
- Instrument logging and include request IDs in responses.
Production checklist
- Endpoints named with plural nouns; consistent casing.
- Every route has method, status codes, example requests/responses.
- Pagination documented and tested for large data.
- Idempotency implemented for non-idempotent operations.
- Authentication and authorization verified per route.
- Input validation with clear error messages.
- Rate limiting and sensible timeouts.
- Observability: request IDs, structured logs, metrics.
- Backward-compatible changes where possible; version if breaking.
- Security review: no sensitive data in errors or logs.
Exercises
Try these on your own. Solutions are available below each exercise in the exercises section.
- Design a ticketing API: Endpoints for events and tickets (create/list events, purchase ticket with idempotency, refund ticket, list tickets with filters). Include request/response examples, status codes, and headers.
- Error model and versioning: Propose a standard error format and plan a non-breaking change where a field is renamed. Show responses and deprecation approach.
Common mistakes and self-check
- Overloading POST for everything. Self-check: Are you using GET for reads, PATCH for partial updates?
- Inconsistent naming. Self-check: Do collection and item routes follow the same pattern?
- Vague errors. Self-check: Do your errors have a code, message, and actionable details?
- No pagination. Self-check: Can any list return thousands of items? Add pagination.
- Breaking clients silently. Self-check: Did you remove or rename fields without a version bump?
- No idempotency for payments/orders. Self-check: Can the client safely retry a request?
Practical projects
- Build a minimal e-commerce API with products, carts, orders. Include idempotent checkout.
- Create a blog API with cursor pagination, search filter, and tag-based filtering.
- Instrument request IDs and structured JSON logs; verify errors include request_id.
Learning path
- Start here: API design basics and patterns (this page).
- Then: Data modeling and validation.
- Next: Authentication/authorization fundamentals (tokens, scopes, roles).
- Finally: Performance and caching (ETags, conditional requests, cache headers).
Who this is for
- Aspiring and junior Backend Engineers designing their first services.
- Full-stack developers who need to create or evolve APIs.
- Engineers preparing for system design interviews.
Prerequisites
- Basic HTTP knowledge (methods, headers, status codes).
- Comfort with JSON and a server-side language.
- Ability to run and test HTTP requests (CLI or any REST client).
Next steps
- Complete the exercises below, then take the Quick Test at the end.
- Note: The Quick Test is available to everyone. Progress is saved only when you are logged in.
Mini challenge
Design one endpoint to archive a project resource. Should it be PATCH or POST? What status code and body will you return? Write your answer, then compare with the guidance below.
Suggested answer
PATCH /v1/projects/{id} with body {"archived": true}. Return 200 OK with updated resource, or 204 No Content if no body. If archiving triggers a long-running job, accept POST /v1/projects/{id}:archive returning 202 Accepted with a job resource.