History of REST
In the year 2000, Roy Fielding published his doctoral dissertation at the University of California, Irvine, titled "Architectural Styles and the Design of Network-based Software Architectures." Chapter 5 of that dissertation introduced Representational State Transfer (REST) as an architectural style for distributed hypermedia systems. It was not a protocol, not a specification, and not a framework — it was a set of constraints that, when applied to the design of web services, produce systems that are scalable, simple, and evolvable.
Fielding was not an ivory-tower academic. He was one of the principal authors of the HTTP/1.1 specification (RFC 2616) and a co-founder of the Apache HTTP Server project. REST was his formalization of the architectural principles that made the World Wide Web work.
SOAP vs REST: The Protocol Wars
Before REST gained mainstream adoption, the dominant paradigm for web services was SOAP (Simple Object Access Protocol). SOAP used XML for message formatting, WSDL (Web Services Description Language) for service contracts, and operated as an RPC (Remote Procedure Call) mechanism tunneled over HTTP POST.
Think of SOAP as sending a letter through a bureaucratic postal system: you fill out a standardized envelope (XML), include a cover sheet describing the contents (WSDL), and the post office (HTTP) does not care what is inside — it just carries the envelope. REST, by contrast, is like using the web itself: you navigate to an address (URL), the type of action is the HTTP method (GET to read, POST to create), and the content comes in whatever format the server provides (JSON, XML, HTML).
| Aspect | SOAP | REST |
|---|---|---|
| Message format | XML only | JSON, XML, HTML, plain text |
| Transport | HTTP, SMTP, TCP | HTTP (by convention) |
| Contract | WSDL (strict, machine-readable) | OpenAPI/Swagger (optional) |
| State | Can be stateful (WS-* extensions) | Stateless (each request is self-contained) |
| Caching | Not built-in | Leverages HTTP caching (ETags, Cache-Control) |
| Adoption (2024) | Legacy enterprise systems | Dominant for web and mobile APIs |
The Richardson Maturity Model
Leonard Richardson proposed a maturity model for REST APIs with four levels. Most APIs that call themselves "RESTful" are actually at Level 2. True REST (Level 3) is rare in practice.
| Level | Name | Description | Example |
|---|---|---|---|
| 0 | The Swamp of POX | One URL, one HTTP method (POST), XML payloads | SOAP-style RPC over HTTP |
| 1 | Resources | Individual URLs for resources, but only POST | POST /users/create, POST /users/delete |
| 2 | HTTP Verbs | Proper use of GET, POST, PUT, DELETE + status codes | GET /users/42, DELETE /users/42 |
| 3 | Hypermedia (HATEOAS) | Responses include links to related actions/resources | Response body contains "_links": {...} |
REST Architectural Principles
Fielding defined six constraints for REST. An API that violates any of these is, by definition, not RESTful (though it may still be perfectly useful).
1. Client-Server Separation
The client and server are independent. The client does not know how data is stored; the server does not know how data is displayed. This separation allows each to evolve independently.
2. Statelessness
Every request from the client must contain all the information needed to process it. The server does not store session state between requests. Authentication tokens, pagination cursors, and filter parameters must all be sent with each request.
Think of a stateless API like a new cashier at a fast-food restaurant who has no memory between orders. Every customer must state their complete order from scratch, including any special instructions, every single time. This seems inefficient for the customer, but it means the restaurant can replace any cashier at any time, add more cashiers during rush hour, and never worry about one cashier's notepad getting lost.
3. Cacheability
Responses must declare whether they are cacheable or not. This is done via HTTP headers like Cache-Control, ETag, and Last-Modified.
4. Uniform Interface
The interface between client and server is standardized. This is the most fundamental constraint and consists of four sub-constraints: identification of resources (URLs), manipulation through representations (JSON/XML), self-descriptive messages (Content-Type, status codes), and HATEOAS.
5. Layered System
The client cannot tell whether it is connected directly to the end server or to an intermediary (load balancer, CDN, API gateway). Each layer only knows about the adjacent layer.
6. Code on Demand (Optional)
Servers can optionally extend client functionality by sending executable code (JavaScript). This is the only optional constraint and is rarely used in API design.
// Example: a fully stateless request
// Every piece of information the server needs is in the request itself
const response = await fetch('https://api.example.com/v1/users?page=2&limit=20', {
method: 'GET',
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...',
'Accept': 'application/json',
'Accept-Language': 'en-US',
'X-Request-ID': 'req-abc-123-def-456'
}
});
Resource Design & URL Structure
Resources are the fundamental abstraction in REST. A resource is any concept that can be named: a user, an order, a product, a search result. The URL (Uniform Resource Locator) is the address of a resource.
URL Design Rules
- Use nouns, not verbs:
/usersnot/getUsers - Use plural nouns:
/usersnot/user - Use hierarchical structure for relationships:
/users/42/orders - Use kebab-case:
/order-itemsnot/orderItems - Avoid deep nesting (max 2-3 levels):
/users/42/ordersis fine;/users/42/orders/99/items/7/reviewsis too deep - Use query parameters for filtering, sorting, and pagination:
/users?role=admin&sort=-created_at&page=2
# Good URL design
GET /api/v1/users # List all users
GET /api/v1/users/42 # Get user 42
POST /api/v1/users # Create a user
PUT /api/v1/users/42 # Replace user 42
PATCH /api/v1/users/42 # Partially update user 42
DELETE /api/v1/users/42 # Delete user 42
GET /api/v1/users/42/orders # List user 42's orders
GET /api/v1/users/42/orders/99 # Get order 99 of user 42
# Filtering, sorting, pagination via query params
GET /api/v1/products?category=electronics&min_price=100&sort=-rating&page=3&limit=20
# Bad URL design (common mistakes)
GET /api/v1/getUser/42 # Verb in URL
POST /api/v1/users/42/delete # Action in URL (use DELETE method)
GET /api/v1/user # Singular noun
Handling Non-CRUD Operations
Not everything maps neatly to CRUD. For actions that do not fit the resource model, you have several options:
# Option 1: Treat the action as a sub-resource
POST /api/v1/users/42/activate # Activate a user account
POST /api/v1/orders/99/cancel # Cancel an order
POST /api/v1/payments/77/refund # Refund a payment
# Option 2: Use a controller resource
POST /api/v1/search # Complex search with body params
POST /api/v1/batch # Batch operations
# Option 3: Use PATCH with a state change
PATCH /api/v1/users/42
# Body: { "status": "active" }
POST /v1/charges/ch_xxx/refund treats "refund" as an action on a charge resource.
HTTP Methods & Status Codes
HTTP Methods
| Method | CRUD | Idempotent | Safe | Request Body | Response Body |
|---|---|---|---|---|---|
| GET | Read | Yes | Yes | No | Yes |
| POST | Create | No | No | Yes | Yes |
| PUT | Replace | Yes | No | Yes | Yes |
| PATCH | Partial update | No* | No | Yes | Yes |
| DELETE | Delete | Yes | No | Optional | Optional |
| HEAD | Read metadata | Yes | Yes | No | No (headers only) |
| OPTIONS | Describe | Yes | Yes | No | Yes |
Idempotent means calling the method multiple times has the same effect as calling it once. PUT /users/42 with the same body always produces the same result. POST /users with the same body may create a duplicate. PATCH is technically not idempotent because the effect depends on the current state, though many implementations are idempotent in practice.
Status Codes
// Express.js examples showing proper status code usage
// 200 OK — successful GET or PATCH
app.get('/api/v1/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.status(200).json(user);
});
// 201 Created — successful POST (include Location header)
app.post('/api/v1/users', async (req, res) => {
const user = await User.create(req.body);
res.status(201)
.location(`/api/v1/users/${user.id}`)
.json(user);
});
// 204 No Content — successful DELETE
app.delete('/api/v1/users/:id', async (req, res) => {
await User.destroy({ where: { id: req.params.id } });
res.status(204).send();
});
// 400 Bad Request — invalid input
// 401 Unauthorized — no or invalid authentication
// 403 Forbidden — authenticated but not authorized
// 404 Not Found — resource does not exist
// 409 Conflict — e.g., duplicate email
// 422 Unprocessable Entity — valid JSON but fails validation
// 429 Too Many Requests — rate limit exceeded
// 500 Internal Server Error — unhandled server error
Authentication & Authorization
Authentication verifies who you are. Authorization determines what you are allowed to do. REST APIs commonly use one of four authentication mechanisms.
API Keys
# API key in header (preferred)
curl -H "X-API-Key: sk_live_abc123def456" \
https://api.example.com/v1/users
# API key in query parameter (avoid — logged in URLs and server logs)
curl https://api.example.com/v1/users?api_key=sk_live_abc123def456
OAuth 2.0 Authorization Code Flow
# Step 1: Redirect user to authorization server
# Browser navigates to:
https://auth.example.com/authorize?
response_type=code&
client_id=your_client_id&
redirect_uri=https://yourapp.com/callback&
scope=read:users write:users&
state=random_csrf_token
# Step 2: User grants permission, auth server redirects to callback
# https://yourapp.com/callback?code=AUTH_CODE_HERE&state=random_csrf_token
# Step 3: Exchange authorization code for access token
curl -X POST https://auth.example.com/token \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE_HERE" \
-d "client_id=your_client_id" \
-d "client_secret=your_client_secret" \
-d "redirect_uri=https://yourapp.com/callback"
# Step 4: Use the access token
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
https://api.example.com/v1/users
JWT (JSON Web Tokens)
// JWT structure: header.payload.signature
// Example payload:
{
"sub": "user_42",
"name": "Wasil Zafar",
"role": "admin",
"iat": 1711929600, // issued at
"exp": 1711933200 // expires in 1 hour
}
// Server-side: verify and decode JWT
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"
if (!token) return res.status(401).json({ error: 'Token required' });
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid token' });
req.user = user;
next();
});
}
Comparison Table
| Method | Complexity | Best For | Security Notes |
|---|---|---|---|
| API Key | Low | Server-to-server, internal APIs | No user context; easy to leak; rotate regularly |
| Basic Auth | Low | Simple internal tools | Base64 encoded (not encrypted); requires HTTPS |
| OAuth 2.0 | High | Third-party access, social login | Industry standard; supports scoped permissions |
| JWT Bearer | Medium | Microservices, SPAs, mobile apps | Stateless; cannot be revoked without blocklist; set short expiry |
API Versioning Strategies
APIs evolve. New fields are added, old fields are deprecated, response structures change. Versioning ensures existing clients continue to work while new clients use the latest version.
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URI Path | /api/v1/users |
Simple, visible, cacheable | URL changes for every version; breaks bookmarks |
| Query Parameter | /api/users?version=1 |
Optional; default to latest | Easy to forget; less visible |
| Custom Header | X-API-Version: 1 |
Clean URLs; no URL changes | Hidden; harder to test in browser |
| Accept Header (Media Type) | Accept: application/vnd.example.v1+json |
Most RESTful; content negotiation | Complex; hard to test; poor tooling support |
// Express.js: URI path versioning (most common)
const v1Router = require('./routes/v1');
const v2Router = require('./routes/v2');
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// Express.js: Header-based versioning
app.use('/api/users', (req, res, next) => {
const version = req.headers['x-api-version'] || '2'; // default to latest
if (version === '1') {
return v1UserHandler(req, res, next);
}
return v2UserHandler(req, res, next);
});
/v1/) but with a twist: they version at the API level, not the endpoint level. Individual endpoints evolve through dated API versions (e.g., Stripe-Version: 2024-01-01) sent as a header. This lets them make incremental changes without bumping the entire API version. Most teams should start with URI path versioning and only add header-based versioning if they need finer-grained control.
Pagination, Filtering & Sorting
Offset-Based Pagination
# Request page 3, 20 items per page
GET /api/v1/products?page=3&limit=20
# Alternative: offset + limit
GET /api/v1/products?offset=40&limit=20
{
"data": [
{ "id": 41, "name": "Widget A", "price": 29.99 },
{ "id": 42, "name": "Widget B", "price": 39.99 }
],
"pagination": {
"page": 3,
"limit": 20,
"total": 157,
"total_pages": 8,
"has_next": true,
"has_prev": true
},
"_links": {
"self": "/api/v1/products?page=3&limit=20",
"first": "/api/v1/products?page=1&limit=20",
"prev": "/api/v1/products?page=2&limit=20",
"next": "/api/v1/products?page=4&limit=20",
"last": "/api/v1/products?page=8&limit=20"
}
}
Cursor-Based Pagination
Offset pagination breaks down with large datasets: OFFSET 1000000 in SQL forces the database to scan and discard 1 million rows. Cursor-based pagination uses an opaque token (usually a Base64-encoded value from the last item) to efficiently fetch the next page.
# First request
GET /api/v1/events?limit=50
# Response includes a cursor for the next page
# "next_cursor": "eyJpZCI6MTUwLCJ0cyI6IjIwMjQtMDMtMTVUMTQ6MDA6MDBaIn0="
# Subsequent request
GET /api/v1/events?limit=50&cursor=eyJpZCI6MTUwLCJ0cyI6IjIwMjQtMDMtMTVUMTQ6MDA6MDBaIn0=
// Server-side cursor implementation (Node.js + PostgreSQL)
async function getEvents(cursor, limit = 50) {
let query = 'SELECT * FROM events';
const params = [];
if (cursor) {
const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString());
query += ' WHERE (created_at, id) < ($1, $2)';
params.push(decoded.ts, decoded.id);
}
query += ' ORDER BY created_at DESC, id DESC LIMIT $' + (params.length + 1);
params.push(limit + 1); // Fetch one extra to determine if there's a next page
const rows = await db.query(query, params);
const hasNext = rows.length > limit;
const data = hasNext ? rows.slice(0, limit) : rows;
const nextCursor = hasNext
? Buffer.from(JSON.stringify({
ts: data[data.length - 1].created_at,
id: data[data.length - 1].id
})).toString('base64')
: null;
return { data, next_cursor: nextCursor, has_next: hasNext };
}
Filtering and Sorting
# Filtering by multiple fields
GET /api/v1/products?category=electronics&brand=sony&min_price=100&max_price=500
# Sorting (prefix with - for descending)
GET /api/v1/products?sort=-price,name
# Field selection (sparse fieldsets)
GET /api/v1/users?fields=id,name,email
# Full-text search
GET /api/v1/products?q=wireless+headphones
Error Handling & Response Format
RFC 7807: Problem Details for HTTP APIs
RFC 7807 (updated by RFC 9457) defines a standard format for error responses. Using a standard format means clients can parse errors consistently across different APIs.
{
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Error",
"status": 422,
"detail": "The request body contains invalid fields.",
"instance": "/api/v1/users",
"errors": [
{
"field": "email",
"message": "Must be a valid email address",
"value": "not-an-email"
},
{
"field": "age",
"message": "Must be a positive integer",
"value": -5
}
]
}
Custom Error Envelope
Many APIs use a custom error envelope that wraps both success and error responses in a consistent structure:
// Express.js error handling middleware
class AppError extends Error {
constructor(statusCode, code, message, details = null) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.details = details;
}
}
// Centralized error handler
app.use((err, req, res, next) => {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: {
code: err.code,
message: err.message,
details: err.details,
request_id: req.id,
timestamp: new Date().toISOString()
}
});
}
// Unhandled errors — never expose stack traces in production
console.error('Unhandled error:', err);
res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred.',
request_id: req.id,
timestamp: new Date().toISOString()
}
});
});
// Usage in a route
app.post('/api/v1/users', async (req, res, next) => {
try {
const existing = await User.findByEmail(req.body.email);
if (existing) {
throw new AppError(409, 'EMAIL_EXISTS',
'A user with this email address already exists.',
{ email: req.body.email });
}
const user = await User.create(req.body);
res.status(201).json({ data: user });
} catch (err) {
next(err);
}
});
Rate Limiting & Throttling
Rate limiting protects your API from abuse, prevents resource exhaustion, and ensures fair usage across all clients. Without rate limiting, a single misbehaving client can degrade performance for everyone.
Common Algorithms
| Algorithm | How It Works | Pros | Cons |
|---|---|---|---|
| Fixed Window | Count requests in fixed time windows (e.g., per minute) | Simple to implement | Burst at window boundaries (2x burst possible) |
| Sliding Window | Weighted count across current + previous window | Smoother than fixed window | Slightly more complex |
| Token Bucket | Tokens added at fixed rate; each request consumes a token | Allows controlled bursts; smooth rate | Requires tracking bucket state |
| Leaky Bucket | Requests enter a queue; processed at fixed rate | Perfectly smooth output rate | Adds latency; queue can fill up |
HTTP Headers for Rate Limiting
# Standard rate limit headers (draft-ietf-httpapi-ratelimit-headers)
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000 # Maximum requests per window
X-RateLimit-Remaining: 742 # Requests remaining in current window
X-RateLimit-Reset: 1711933200 # Unix timestamp when window resets
Retry-After: 30 # Seconds to wait (only on 429 responses)
# When rate limit is exceeded:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711933200
Retry-After: 30
Content-Type: application/json
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "You have exceeded the rate limit of 1000 requests per hour.",
"retry_after": 30
}
}
// Express.js rate limiting with express-rate-limit
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 1000, // 1000 requests per window per IP
standardHeaders: true, // Return X-RateLimit-* headers
legacyHeaders: false,
message: {
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests. Please try again later.'
}
},
keyGenerator: (req) => {
// Rate limit by API key if present, otherwise by IP
return req.headers['x-api-key'] || req.ip;
}
});
app.use('/api/', apiLimiter);
OpenAPI & Documentation
The OpenAPI Specification (formerly Swagger Specification) is the industry standard for describing REST APIs. An OpenAPI document is a machine-readable YAML or JSON file that defines every endpoint, request/response schema, authentication method, and error format.
openapi: 3.0.3
info:
title: E-Commerce API
version: 1.0.0
description: API for managing products, orders, and users.
contact:
name: API Support
email: api-support@example.com
servers:
- url: https://api.example.com/v1
description: Production
security:
- bearerAuth: []
paths:
/products:
get:
summary: List products
tags: [Products]
parameters:
- name: category
in: query
schema:
type: string
description: Filter by category
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Product'
pagination:
$ref: '#/components/schemas/Pagination'
'401':
$ref: '#/components/responses/Unauthorized'
post:
summary: Create a product
tags: [Products]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateProduct'
responses:
'201':
description: Product created
headers:
Location:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
Product:
type: object
properties:
id:
type: integer
name:
type: string
price:
type: number
format: float
category:
type: string
CreateProduct:
type: object
required: [name, price]
properties:
name:
type: string
minLength: 1
maxLength: 200
price:
type: number
minimum: 0.01
category:
type: string
Pagination:
type: object
properties:
page:
type: integer
limit:
type: integer
total:
type: integer
total_pages:
type: integer
responses:
Unauthorized:
description: Authentication required
content:
application/json:
schema:
type: object
properties:
error:
type: object
properties:
code:
type: string
example: UNAUTHORIZED
message:
type: string
example: Invalid or missing authentication token
Case Studies
Case Study 1: Stripe API
Stripe's API is widely considered the gold standard of REST API design. Key design decisions that make it exceptional:
- Predictable resource URLs:
/v1/charges,/v1/customers,/v1/subscriptions. Every resource follows the same pattern. - Consistent object structure: Every object has an
id,object(type name),created(timestamp), andlivemode(boolean) field. - Expandable objects:
GET /v1/charges/ch_xxx?expand[]=customer&expand[]=invoicelets clients fetch related objects in a single request, reducing N+1 query problems. - Idempotency keys:
POSTrequests accept anIdempotency-Keyheader. If you retry a request with the same key, Stripe returns the original response instead of creating a duplicate charge. - Versioning via date:
Stripe-Version: 2024-04-10pins your integration to a specific API version. Breaking changes are released as new dated versions. - Detailed error messages: Every error includes
type,code,message,param(the field that caused the error), anddoc_url(link to documentation).
Case Study 2: GitHub API
GitHub's REST API (v3) demonstrates practical HATEOAS and advanced pagination:
- Hypermedia navigation: Every response includes a
urlfield and related resource URLs (e.g., a repository object includesissues_url,pulls_url,branches_url). - Link header pagination:
Link: <https://api.github.com/repos/...?page=2>; rel="next", <...?page=5>; rel="last" - Conditional requests: Heavy use of
ETagandIf-None-Matchheaders. Cached responses do not count against rate limits. - Granular OAuth scopes:
repo,repo:status,public_repo,read:org— clients request only the permissions they need. - Rate limiting: 5,000 requests/hour for authenticated users, 60/hour for unauthenticated. Rate limit headers on every response.
Case Study 3: Twitter (X) API v2
Twitter's API v2 redesign (launched 2020) addressed shortcomings of v1.1:
- Field selection:
GET /2/tweets?ids=123&tweet.fields=created_at,public_metrics&expansions=author_id&user.fields=name,username. Clients specify exactly which fields they want, reducing payload size by up to 80%. - Expansion system: Related objects (author, media, referenced tweets) are included in an
includesobject rather than nested inline, preventing duplication. - Tiered access levels: Free, Basic ($100/month), Pro ($5,000/month), Enterprise. Each tier has different rate limits and endpoint access.
Exercises
Design a Bookstore API
Design the URL structure and HTTP methods for a bookstore API. Support the following operations: list all books, get a single book, create a book, update a book, delete a book, list all reviews for a book, add a review, search books by title or author, and filter books by genre. Write out each endpoint with its HTTP method, URL, request body (if any), and expected response status code.
Build a Paginated API with Express
Implement a REST API with Express.js that serves a list of 1,000 products from an in-memory array. Implement both offset-based and cursor-based pagination. Add filtering by category and price range, and sorting by price or name. Include proper Link headers for pagination navigation. Write tests that verify: (1) correct page sizes, (2) last page handling, (3) empty results for out-of-range pages, and (4) cursor stability when new items are inserted.
Design an API with OAuth 2.0 and Rate Limiting
Design and implement a complete API for a task management application with: (1) JWT-based authentication with access and refresh tokens, (2) role-based authorization (admin, member, viewer), (3) token bucket rate limiting with different limits per role, (4) RFC 7807 error responses, (5) an OpenAPI 3.0 specification file, and (6) idempotency keys for POST requests. Deploy it and test it with curl, Postman, and an auto-generated client from the OpenAPI spec.
API Design Document Generator
Use this tool to document your REST API design decisions — versioning strategy, authentication method, error format, endpoints, and rate limiting configuration. Download as Word, Excel, PDF, or PowerPoint for team review or stakeholder communication.
Document your API design configuration and export for review. All data stays in your browser — nothing is sent to any server.
All data stays in your browser. Nothing is sent to or stored on any server.
Conclusion & Resources
REST is not a technology — it is an architectural style. The best REST APIs are not the ones that follow every constraint perfectly, but the ones that are consistent, predictable, well-documented, and easy to use. We have covered the full spectrum: from Fielding's original principles to the practical patterns used by Stripe, GitHub, and Twitter in production.
The most important takeaways:
- Resources are nouns, methods are verbs. Let HTTP do the heavy lifting.
- Be consistent. If
/usersreturns a list and/users/42returns a single user, every resource should follow this pattern. - Use proper status codes. 200 for success, 201 for creation, 4xx for client errors, 5xx for server errors. Never return 200 with an error body.
- Design for the consumer. Your API's users are other developers. Good documentation, clear error messages, and predictable behavior save them hours of debugging.
- Version from day one. It is far easier to add
/v1/at the start than to retrofit versioning after clients depend on your endpoints.
Recommended Resources
- Fielding's Dissertation — Chapter 5: REST
- Stripe API Reference — stripe.com/docs/api (the gold standard)
- OpenAPI Specification — spec.openapis.org
- RFC 7807 — Problem Details for HTTP APIs
- Google API Design Guide — cloud.google.com/apis/design
- Microsoft REST API Guidelines — github.com/microsoft/api-guidelines