REST API Design
📖 Concept
REST (Representational State Transfer) is the most widely used API architecture style. It's not a protocol — it's a set of architectural constraints that, when followed, produce scalable, cacheable, and maintainable APIs.
REST Constraints
- Client-Server: Separation of concerns between frontend and backend
- Stateless: Each request contains all information needed to process it
- Cacheable: Responses must define whether they're cacheable
- Uniform Interface: Consistent URL patterns, HTTP methods, and response formats
- Layered System: Client doesn't know if it's talking to the actual server or a proxy/cache
- Code on Demand (optional): Server can send executable code to the client
RESTful URL Design
| Action | Method | URL | Description |
|---|---|---|---|
| List users | GET | /api/v1/users |
Get all users |
| Get one user | GET | /api/v1/users/123 |
Get user with ID 123 |
| Create user | POST | /api/v1/users |
Create a new user |
| Update user | PUT | /api/v1/users/123 |
Replace user 123 entirely |
| Partial update | PATCH | /api/v1/users/123 |
Update specific fields |
| Delete user | DELETE | /api/v1/users/123 |
Delete user 123 |
| User's posts | GET | /api/v1/users/123/posts |
Nested resource |
Pagination Patterns
| Pattern | Example | Pros | Cons |
|---|---|---|---|
| Offset | ?page=3&limit=20 |
Simple, familiar | Slow for large offsets, inconsistent with inserts |
| Cursor | ?cursor=eyJpZCI6MTIzfQ&limit=20 |
Consistent, fast | Can't jump to arbitrary page |
| Keyset | ?after_id=123&limit=20 |
Very fast (uses index) | Only forward pagination |
Versioning Strategies
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL path | /api/v1/users |
Explicit, easy to route | URL pollution |
| Header | Accept: application/vnd.api.v1+json |
Clean URLs | Hidden, harder to test |
| Query param | /api/users?version=1 |
Easy to use | Easy to forget |
Industry standard: Most companies (Stripe, GitHub, Twitter) use URL path versioning because it's the most explicit and developer-friendly.
💻 Code Example
1// ============================================2// REST API Design — Production Patterns3// ============================================45const express = require('express');6const app = express();7app.use(express.json());89// ---------- Resource-Based URL Design ----------1011// ❌ BAD: Verb-based URLs (RPC style, not REST)12app.get('/api/getAllUsers', () => {}); // verb in URL13app.post('/api/createUser', () => {}); // verb in URL14app.post('/api/deleteUser', () => {}); // POST for delete?!15app.get('/api/getUserPosts', () => {}); // action-based1617// ✅ GOOD: Resource-based URLs with HTTP methods18// Users resource19app.get('/api/v1/users', listUsers); // List20app.get('/api/v1/users/:id', getUser); // Read21app.post('/api/v1/users', createUser); // Create22app.put('/api/v1/users/:id', updateUser); // Update (full)23app.patch('/api/v1/users/:id', patchUser); // Update (partial)24app.delete('/api/v1/users/:id', deleteUser); // Delete2526// Nested resources: user's posts27app.get('/api/v1/users/:userId/posts', getUserPosts);28app.post('/api/v1/users/:userId/posts', createUserPost);2930// ---------- Pagination (Cursor-based) ----------3132async function listUsers(req, res) {33 const limit = Math.min(parseInt(req.query.limit) || 20, 100); // Max 10034 const cursor = req.query.cursor; // Base64 encoded last-seen ID3536 let query = 'SELECT * FROM users';37 const params = [];3839 if (cursor) {40 const decodedCursor = JSON.parse(41 Buffer.from(cursor, 'base64').toString('utf-8')42 );43 query += ' WHERE id > ?';44 params.push(decodedCursor.lastId);45 }4647 query += ' ORDER BY id ASC LIMIT ?';48 params.push(limit + 1); // Fetch one extra to check if there's a next page4950 const users = await db.query(query, params);51 const hasNext = users.length > limit;52 const results = hasNext ? users.slice(0, limit) : users;5354 const nextCursor = hasNext55 ? Buffer.from(JSON.stringify({ lastId: results[results.length - 1].id })).toString('base64')56 : null;5758 res.json({59 data: results,60 pagination: {61 limit,62 hasNext,63 nextCursor,64 // Usage: GET /api/v1/users?cursor={nextCursor}&limit=2065 },66 });67}6869// ---------- Consistent Error Response Format ----------7071// ✅ GOOD: Structured error responses72function errorResponse(res, status, code, message, details = null) {73 const response = {74 error: {75 status,76 code, // Machine-readable error code77 message, // Human-readable message78 timestamp: new Date().toISOString(),79 },80 };81 if (details) response.error.details = details;82 return res.status(status).json(response);83}8485// Usage in handler:86async function getUser(req, res) {87 const { id } = req.params;8889 if (!isValidId(id)) {90 return errorResponse(res, 400, 'INVALID_ID', 'User ID must be a positive integer');91 }9293 const user = await db.findUser(id);94 if (!user) {95 return errorResponse(res, 404, 'USER_NOT_FOUND', `User with ID \${id} not found`);96 }9798 res.json({99 data: user,100 links: {101 self: `/api/v1/users/\${id}`,102 posts: `/api/v1/users/\${id}/posts`,103 },104 });105}106107// ---------- HATEOAS (Hypermedia) ----------108109async function createUser(req, res) {110 const user = await db.createUser(req.body);111112 res.status(201).json({113 data: user,114 links: {115 self: `/api/v1/users/\${user.id}`,116 posts: `/api/v1/users/\${user.id}/posts`,117 update: { method: 'PUT', href: `/api/v1/users/\${user.id}` },118 delete: { method: 'DELETE', href: `/api/v1/users/\${user.id}` },119 },120 });121}122123// ---------- Filtering, Sorting, Field Selection ----------124125// GET /api/v1/users?status=active&sort=-created_at&fields=id,name,email126async function listUsersAdvanced(req, res) {127 const { status, sort, fields, q } = req.query;128129 let query = 'SELECT ';130131 // Field selection (reduce payload)132 query += fields ? fields.split(',').map(f => `\${f}`).join(', ') : '*';133 query += ' FROM users WHERE 1=1';134135 // Filtering136 if (status) query += ` AND status = '\${status}'`;137138 // Search139 if (q) query += ` AND (name LIKE '%\${q}%' OR email LIKE '%\${q}%')`;140141 // Sorting (prefix with - for descending)142 if (sort) {143 const direction = sort.startsWith('-') ? 'DESC' : 'ASC';144 const field = sort.replace('-', '');145 query += ` ORDER BY \${field} \${direction}`;146 }147148 const users = await db.query(query);149 res.json({ data: users, meta: { total: users.length } });150}151152function isValidId(id) {153 return Number.isInteger(Number(id)) && Number(id) > 0;154}155156function updateUser() {}157function patchUser() {}158function deleteUser() {}159function getUserPosts() {}160function createUserPost() {}161162app.listen(3000);
🏋️ Practice Exercise
Design a REST API: Design the complete REST API for a bookstore application: books, authors, reviews, and user wishlists. Include URL paths, HTTP methods, request/response bodies, and status codes.
Pagination Comparison: Implement cursor-based pagination for a messages endpoint where new messages are constantly being added. Explain why offset pagination would fail here.
Error Standardization: Design an error response format that includes: error code, human-readable message, field-level validation errors, request ID for debugging, and documentation link. Show 5 example error responses.
Versioning Migration: Your API v1 returns user names as a single
namefield. V2 splits it intofirstNameandlastName. Design the migration strategy to support both versions simultaneously.Rate Limiting Design: Design a rate limiting strategy for your REST API with different tiers: free (100 req/hour), pro (1000 req/hour), enterprise (unlimited). How would you implement this?
⚠️ Common Mistakes
Using verbs in URLs (/api/getUsers) instead of nouns (/api/users) — REST uses HTTP methods to convey the action, not the URL. URLs should represent resources, not operations.
Returning 200 OK with an empty body for creation — use 201 Created with the created resource in the body and a Location header pointing to the new resource.
Not versioning APIs from the start — adding versioning later is painful. Always start with /api/v1/ even if you think you'll never need v2.
Using offset pagination for large datasets — OFFSET 1000000 requires the database to scan and skip 1M rows. Use cursor-based pagination for performance at scale.
Inconsistent response formats — sometimes returning {users: [...]}, sometimes {data: [...]}, sometimes just [...]. Pick ONE envelope format and use it everywhere.
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for REST API Design