REST API Design

0/4 in this phase0/45 across the roadmap

📖 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

  1. Client-Server: Separation of concerns between frontend and backend
  2. Stateless: Each request contains all information needed to process it
  3. Cacheable: Responses must define whether they're cacheable
  4. Uniform Interface: Consistent URL patterns, HTTP methods, and response formats
  5. Layered System: Client doesn't know if it's talking to the actual server or a proxy/cache
  6. 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

codeTap to expand ⛶
1// ============================================
2// REST API Design — Production Patterns
3// ============================================
4
5const express = require('express');
6const app = express();
7app.use(express.json());
8
9// ---------- Resource-Based URL Design ----------
10
11// ❌ BAD: Verb-based URLs (RPC style, not REST)
12app.get('/api/getAllUsers', () => {}); // verb in URL
13app.post('/api/createUser', () => {}); // verb in URL
14app.post('/api/deleteUser', () => {}); // POST for delete?!
15app.get('/api/getUserPosts', () => {}); // action-based
16
17// ✅ GOOD: Resource-based URLs with HTTP methods
18// Users resource
19app.get('/api/v1/users', listUsers); // List
20app.get('/api/v1/users/:id', getUser); // Read
21app.post('/api/v1/users', createUser); // Create
22app.put('/api/v1/users/:id', updateUser); // Update (full)
23app.patch('/api/v1/users/:id', patchUser); // Update (partial)
24app.delete('/api/v1/users/:id', deleteUser); // Delete
25
26// Nested resources: user's posts
27app.get('/api/v1/users/:userId/posts', getUserPosts);
28app.post('/api/v1/users/:userId/posts', createUserPost);
29
30// ---------- Pagination (Cursor-based) ----------
31
32async function listUsers(req, res) {
33 const limit = Math.min(parseInt(req.query.limit) || 20, 100); // Max 100
34 const cursor = req.query.cursor; // Base64 encoded last-seen ID
35
36 let query = 'SELECT * FROM users';
37 const params = [];
38
39 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 }
46
47 query += ' ORDER BY id ASC LIMIT ?';
48 params.push(limit + 1); // Fetch one extra to check if there's a next page
49
50 const users = await db.query(query, params);
51 const hasNext = users.length > limit;
52 const results = hasNext ? users.slice(0, limit) : users;
53
54 const nextCursor = hasNext
55 ? Buffer.from(JSON.stringify({ lastId: results[results.length - 1].id })).toString('base64')
56 : null;
57
58 res.json({
59 data: results,
60 pagination: {
61 limit,
62 hasNext,
63 nextCursor,
64 // Usage: GET /api/v1/users?cursor={nextCursor}&limit=20
65 },
66 });
67}
68
69// ---------- Consistent Error Response Format ----------
70
71// ✅ GOOD: Structured error responses
72function errorResponse(res, status, code, message, details = null) {
73 const response = {
74 error: {
75 status,
76 code, // Machine-readable error code
77 message, // Human-readable message
78 timestamp: new Date().toISOString(),
79 },
80 };
81 if (details) response.error.details = details;
82 return res.status(status).json(response);
83}
84
85// Usage in handler:
86async function getUser(req, res) {
87 const { id } = req.params;
88
89 if (!isValidId(id)) {
90 return errorResponse(res, 400, 'INVALID_ID', 'User ID must be a positive integer');
91 }
92
93 const user = await db.findUser(id);
94 if (!user) {
95 return errorResponse(res, 404, 'USER_NOT_FOUND', `User with ID \${id} not found`);
96 }
97
98 res.json({
99 data: user,
100 links: {
101 self: `/api/v1/users/\${id}`,
102 posts: `/api/v1/users/\${id}/posts`,
103 },
104 });
105}
106
107// ---------- HATEOAS (Hypermedia) ----------
108
109async function createUser(req, res) {
110 const user = await db.createUser(req.body);
111
112 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}
122
123// ---------- Filtering, Sorting, Field Selection ----------
124
125// GET /api/v1/users?status=active&sort=-created_at&fields=id,name,email
126async function listUsersAdvanced(req, res) {
127 const { status, sort, fields, q } = req.query;
128
129 let query = 'SELECT ';
130
131 // Field selection (reduce payload)
132 query += fields ? fields.split(',').map(f => `\${f}`).join(', ') : '*';
133 query += ' FROM users WHERE 1=1';
134
135 // Filtering
136 if (status) query += ` AND status = '\${status}'`;
137
138 // Search
139 if (q) query += ` AND (name LIKE '%\${q}%' OR email LIKE '%\${q}%')`;
140
141 // 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 }
147
148 const users = await db.query(query);
149 res.json({ data: users, meta: { total: users.length } });
150}
151
152function isValidId(id) {
153 return Number.isInteger(Number(id)) && Number(id) > 0;
154}
155
156function updateUser() {}
157function patchUser() {}
158function deleteUser() {}
159function getUserPosts() {}
160function createUserPost() {}
161
162app.listen(3000);

🏋️ Practice Exercise

  1. 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.

  2. Pagination Comparison: Implement cursor-based pagination for a messages endpoint where new messages are constantly being added. Explain why offset pagination would fail here.

  3. 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.

  4. Versioning Migration: Your API v1 returns user names as a single name field. V2 splits it into firstName and lastName. Design the migration strategy to support both versions simultaneously.

  5. 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