system-design · intermediate
API Design — Clear Contracts Clients Can Trust
The Central Question
Consider an e-commerce platform running on the APILab platform (apilab.com) where different backend teams implemented endpoints independently:
- Developer A created
GET /get_user_by_id?id=42returning{ "usr": "Alice" }. - Developer B created
POST /create_order_nowreturningHTTP 200 OKwith body{ "status": "FAIL", "msg": "Out of stock" }. - Developer C created
POST /deleteUserreturningHTTP 500 Internal Errorwhen a user ID does not exist.
As the engineering team grows to 50 developers building 10 separate client applications (iOS, Android, Web, Partner Integrations), this ad-hoc lack of design standards causes endless integration bugs, inconsistent client error handling, and accidental database overload.
API Design is the disciplined architectural practice of structuring API resources, HTTP methods, data schemas, error payloads, and pagination mechanisms to create predictable, maintainable software contracts.
This lesson answers one central question: How do engineers design RESTful resource hierarchies, select HTTP methods, structure actionable error schemas, and implement pagination models to build stable, developer-friendly APIs?
The Core Principles of RESTful API Design
The most prevalent API design paradigm for web and mobile backends is REST (Representational State Transfer), which models system functionality around Resources accessed via standard HTTP methods.
flowchart TD
subgraph Nouns: Identifiable System Resources
Users["/v1/users (User Accounts)"]
Orders["/v1/orders (Customer Orders)"]
Products["/v1/products (Catalog Items)"]
end
subgraph Verbs: Standard HTTP Methods
GET["GET: Read Resource State"]
POST["POST: Create New Resource"]
PUT["PUT: Replace Entire Resource"]
PATCH["PATCH: Update Partial Resource"]
DELETE["DELETE: Remove Resource"]
end
Users -->|Exposes| GET
Users -->|Exposes| POST
Orders -->|Exposes| PUT
Orders -->|Exposes| PATCH
Products -->|Exposes| DELETE
Figure 1: Decoupling resource nouns (URIs) from operation verbs (HTTP methods).
1. Model URIs Around Nouns, Not Verbs
API URIs should identify **resources** (nouns) rather than remote procedural actions (verbs):- Poor (RPC Style):
POST /api/getOrdersForUser?userId=901 - Poor (Verb in URI):
POST /api/createNewOrder - Good (RESTful Noun):
GET /v1/users/901/orders - Good (RESTful Noun):
POST /v1/orders
2. Standardize Plural Noun Conventions
Use plural nouns for resource collections (`/users`, `/orders`, `/products`) to maintain consistency across endpoints:GET /v1/products$\rightarrow$ Returns a list of products.GET /v1/products/prod_88$\rightarrow$ Returns productprod_88.POST /v1/products/prod_88/reviews$\rightarrow$ Creates a new review for productprod_88.
HTTP Method Semantics: Safety and Idempotency
In disciplined API design, every HTTP method conveys strict operational semantics regarding Safety and Idempotency:
flowchart LR
subgraph Safe & Idempotent (Read Only)
GET[GET /v1/products]
HEAD[HEAD /v1/products]
end
subgraph Idempotent but Unsafe (Mutates State)
PUT[PUT /v1/users/42]
DELETE[DELETE /v1/users/42]
end
subgraph Neither Safe nor Idempotent
POST[POST /v1/orders]
end
Figure 2: Taxonomy of HTTP methods categorized by Safety and Idempotency properties.
Safety vs. Idempotency Definitions
- Safe Method: An HTTP operation that does not modify system state (read-only). Safe methods can be cached by browsers and CDNs without side effects.
- Idempotent Method: An HTTP operation where executing the request $N$ identical times produces the exact same final server state as executing it once.
HTTP Method Properties Matrix
| Method | Intended Action | Safe? | Idempotent? | Typical Success Status Code |
|---|---|---|---|---|
| GET | Retrieve resource state. | Yes | Yes | 200 OK |
| POST | Create a new resource or execute a complex operation. | No | No | 201 Created |
| PUT | Replace an entire resource payload (full overwrite). | No | Yes | 200 OK / 204 No Content |
| PATCH | Apply partial modifications to an existing resource. | No | No | 200 OK |
| DELETE | Remove a specified resource. | No | Yes | 204 No Content |
Structuring Actionable Error Payloads: RFC 7807
A major cause of client integration failure is inconsistent error payloads. One endpoint returns {"err": "missing_field"}, while another returns an un-parsed stack trace.
High-quality API design standardizes all error responses on RFC 7807 (Problem Details for HTTP APIs).
flowchart TB
ErrResp[HTTP 400 Bad Request Response] --> Type["type: URI pointing to error documentation"]
ErrResp --> Title["title: Short human summary"]
ErrResp --> Status["status: HTTP status code (e.g. 400)"]
ErrResp --> Detail["detail: Specific explanation of this failure"]
ErrResp --> InvalidParams["invalidParams: Array of specific field errors"]
Figure 3: Anatomy of an RFC 7807 compliant JSON error detail payload.
Production RFC 7807 Error Payload Example
{
"type": "https://api.apilab.com/errors/validation-error",
"title": "Invalid Request Parameters",
"status": 400,
"detail": "The request payload failed validation against the order schema.",
"instance": "/v1/orders/err_99102",
"invalidParams": [
{
"name": "items[0].quantity",
"reason": "Quantity must be an integer between 1 and 100."
},
{
"name": "shippingAddressId",
"reason": "Address ID 'addr_00' does not exist for this user."
}
]
}
Why Machine-Readable Error Codes Matter
Clients branch logic based on machine-readable codes (such as `type` URIs or specific error code enums), not free-text human strings. If the backend team updates the human `detail` text from `"Invalid qty"` to `"Quantity out of range"`, client branching logic remains unbroken.Collection Pagination: Offset vs. Cursor Models
List endpoints (GET /v1/orders) that return thousands of records must implement Pagination. Un-paginated list endpoints cause database memory exhaustion and high latency.
flowchart TB
subgraph Offset-Based Pagination
Off1["GET /v1/orders?offset=40&limit=20"] --> OffDB["SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 40"]
Note1["Issue: Performance degrades to O(N); page drift when items inserted"]
end
subgraph Cursor-Based Pagination
Cur1["GET /v1/orders?cursor=ord_9940&limit=20"] --> CurDB["SELECT * FROM orders WHERE id > 'ord_9940' ORDER BY id LIMIT 20"]
Note2["Benefit: Fast O(1) indexed lookup; immune to page drift"]
end
Figure 4: Comparing offset-based SQL scans against indexed cursor-based pagination.
1. Offset-Based Pagination (offset & limit)
The client passes an offset count and page limit (`GET /v1/orders?offset=100&limit=20`).
- Advantages: Simple to implement in SQL (
LIMIT 20 OFFSET 100); allows clients to jump directly to arbitrary page numbers (e.g. Page 5). - Disadvantages:
2. Cursor-Based Pagination (cursor & limit)
The client passes an opaque pointer (typically a base64-encoded string containing a timestamp or unique ID) representing the last seen item (`GET /v1/orders?cursor=eyJpZCI6OSw...&limit=20`).
- Advantages:
- Disadvantages: Clients cannot jump to arbitrary page numbers; they must paginate sequentially.
Pagination Comparison Matrix
| Operational Vector | Offset-Based Pagination | Cursor-Based Pagination |
|---|---|---|
| SQL Query Pattern | LIMIT 20 OFFSET 1000 | WHERE id > :cursor LIMIT 20 |
| Database Execution Time | $O(N)$ — Slow on large datasets. | $O(1)$ — Fast index range scan. |
| Data Consistency | Prone to page drift & missing items. | Perfectly stable during real-time writes. |
| UI Compatibility | Ideal for numbered page links (Page 1, 2, 3). | Ideal for infinite scroll mobile feeds. |
API Versioning Strategies: Path vs. Header vs. Query Parameters
When shipping breaking changes, backend engineering teams choose between three primary API versioning strategies:
flowchart TD
Version[API Versioning Strategies] --> Path["1. URI Path Versioning (/v1/orders vs /v2/orders)"]
Version --> Header["2. Custom Request Header (X-API-Version: 2026-07-24)"]
Version --> Accept["3. Accept Header Content Negotiation (Accept: application/vnd.company.v2+json)"]
Path --> PathAdv["Explicit in browser & logs; easy edge gateway routing."]
Header --> HeaderAdv["Clean URIs; allows date-based API evolution (Stripe style)."]
Accept --> AcceptAdv["Strict REST conformance; complex client configuration."]
Figure 5: Taxonomical comparison of API versioning implementation patterns.
1. URI Path Versioning (/v1/orders)
The major version number is embedded directly into the URI path (`https://api.apilab.com/v1/orders`).
- Why It Prevails: URI path versioning is explicit, easily inspectable in server access logs, and straightforward to route at edge API gateways.
- Edge Gateway Routing: Ingress proxies like Envoy or Kong inspect the leading
/v1prefix to forward traffic to specific microservice target deployment clusters without parsing payload headers.
Complete Worked Example: Go Cursor-Paginated REST API
Let's inspect a production Go implementation of a RESTful collection handler for the APILab platform (apilab.com) demonstrating cursor-based pagination and RFC 7807 error responses.
package main
import (
"encoding/base64"
"encoding/json"
"net/http"
"strconv"
)
type Product struct {
ID string json:"id"
Name string json:"name"
Price float64 json:"price"
}
type PageInfo struct {
NextCursor string json:"nextCursor"
HasNextPage bool json:"hasNextPage"
}
type PaginatedProductsResponse struct {
Data []Product json:"data"
PageInfo PageInfo json:"pageInfo"
}
func ProductsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
limitStr := r.URL.Query().Get("limit")
limit := 20
if limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 {
limit = parsedLimit
}
}
cursor := r.URL.Query().Get("cursor")
lastID := ""
if cursor != "" {
decoded, err := base64.StdEncoding.DecodeString(cursor)
if err == nil {
lastID = string(decoded)
}
}
// Execute O(1) indexed SQL Query: SELECT id, name, price FROM products WHERE id > $1 ORDER BY id ASC LIMIT $2
products := fetchProductsFromDB(lastID, limit+1)
hasNext := false
if len(products) > limit {
hasNext = true
products = products[:limit]
}
nextCursor := ""
if hasNext && len(products) > 0 {
lastProduct := products[len(products)-1]
nextCursor = base64.StdEncoding.EncodeToString([]byte(lastProduct.ID))
}
res := PaginatedProductsResponse{
Data: products,
PageInfo: PageInfo{
NextCursor: nextCursor,
HasNextPage: hasNext,
},
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(res)
}
func fetchProductsFromDB(lastID string, limit int) []Product {
// Database query placeholder...
return []Product{
{ID: "prod_101", Name: "Mechanical Keyboard", Price: 129.99},
{ID: "prod_102", Name: "Wireless Mouse", Price: 59.99},
}
}
Failure Modes and Engineering Mitigations
| Failure Scenario | Root Cause | System Symptom | Detection Signal | Mitigation Strategy |
|---|---|---|---|---|
| 1. Un-paginated List Endpoints | GET /v1/orders executes SELECT * FROM orders without limits. | Database memory exhaustion (OOM crash) during peak query traffic. | High database memory utilization and long p99 response times on list endpoints. | Enforce mandatory default (limit=20) and maximum (limit=100) page size caps at the API layer. |
| 2. Deep Offset Pagination Slowdown | Client queries GET /v1/catalog?offset=500000. | Database CPU hits 100% as SQL engine scans 500,000 un-indexed rows. | High database lock wait times and slow query log alerts. | Migrate high-volume collection endpoints from offset-based to cursor-based pagination. |
| 3. Non-Standard Error Formatting | Microservices return custom error fields (errCode, error_message, msg). | Mobile client crash rate increases due to missing error field handling. | High client-side exception metrics following API release. | Standardize all backend microservices on RFC 7807 Problem Details error middleware. |
| 4. Verb Pollution in REST URIs | Exposing endpoints like /v1/cancelOrder or /v1/updateUser. | Inconsistent URI routing; cache invalidation rules break at edge gateways. | Inconsistent routing metrics and CDN cache miss rates. | Model URIs around plural resources (PATCH /v1/orders/ord_881) with state payload transitions ("status": "CANCELLED"). |
What You Should Remember
- Nouns for URIs, Verbs for HTTP Methods: Name resources with plural nouns (
/v1/orders). Use HTTP methods (GET,POST,PUT,DELETE) to express operations. - Respect Safety and Idempotency:
GETmust be safe and read-only.PUTandDELETEmust be idempotent.POSTis neither safe nor idempotent by default. - Standardize on RFC 7807 Error Schemas: Return machine-readable error codes (
type,status,detail,invalidParams) so clients can branch logic reliably. - Prefer Cursor-Based Pagination for Scale: Use opaque cursor pointers (
WHERE id > cursor LIMIT 20) to maintain $O(1)$ database query performance and prevent page drift. - Enforce Hard Limits on Collections: Never expose an un-paginated collection endpoint. Set mandatory default and maximum page limits.
Glossary of Terms
| Term | Definition |
|---|---|
| API Design | The practice of structuring resource URIs, methods, data schemas, and error handling for APIs. |
| REST (Representational State Transfer) | An architectural style that models system functionality around resources accessed via standard HTTP verbs. |
| Resource | An entity or collection exposed by an API (e.g. /v1/users, /v1/orders). |
| Idempotent Method | An HTTP operation where repeating the request produces the exact same final server state as executing it once. |
| Safe Method | An HTTP operation that reads data without causing state mutations on the server. |
| RFC 7807 | The standard JSON specification for communicating HTTP API error details. |
| Cursor Pagination | Paginating dataset results using an indexed pointer rather than a numeric offset count. |
| Page Drift | The phenomenon where inserted or deleted rows cause users to skip or see duplicate records during offset pagination. |
Practice Scenario and Self-Assessment
Architecture Scenario
You are designing an API for an online learning platform (`learnlab.com`). Students can enroll in courses, track progress, and submit assignments.A junior developer proposes the following endpoints:
POST /api/getStudentCourses?studentId=42POST /api/deleteEnrollment?enrollmentId=90GET /api/getAllAssignments(returns all 800,000 assignments in one JSON array)
Questions:
- Identify the API design flaws in each of the three proposed endpoints.
- Refactor all three endpoints into a clean RESTful specification adhering to REST resource conventions, proper HTTP verbs, and cursor pagination.
Interactive Self-Assessment
cursor), providing fast O(1) database performance and preventing page drift during active writes.">It uses indexed range scans for O(1) database performance and eliminates page drift during concurrent writes.
It allows clients to jump directly to arbitrary page numbers (such as Page 500) faster.
It automatically compresses HTTP JSON payloads using Gzip compression.
It eliminates the need for HTTP status codes on collection endpoints.
The machine-readable 'type' URI code indicating the specific error category.
The human-readable 'detail' text string explaining the error.
The generic HTTP 400 status code alone.
The 'instance' request trace ID assigned to the execution.
What to Learn Next
- API Gateway — Edge Entry for Microservices: Learn how edge gateways enforce authentication, routing, and rate limits.
- REST vs RPC — Wire Protocol Trade-offs: Compare REST JSON vs gRPC Protobuf binary communication.
- REST vs GraphQL — Query Flexibility vs Over-Fetching: Explore GraphQL query flexibilities and DataLoader mechanics.
Track: Software Design and Architecture
Next: Distributed Rate Limiting — Shared Quotas Across Many Pods
By Shubham Jain