REST API Design Principles

📖 Concept

REST (Representational State Transfer) is an architectural style for designing web APIs. A well-designed REST API is intuitive, consistent, and scalable.

Core REST principles:

  1. Resources — Everything is a resource identified by a URL
  2. HTTP methods — Use verbs correctly (GET, POST, PUT, PATCH, DELETE)
  3. Stateless — Each request contains all information needed
  4. Uniform interface — Consistent URL patterns and response formats
  5. HATEOAS — Responses include links to related resources

URL design conventions:

GET    /api/v1/users           → List users (collection)
GET    /api/v1/users/42        → Get user 42 (resource)
POST   /api/v1/users           → Create a user
PUT    /api/v1/users/42        → Replace user 42 (full update)
PATCH  /api/v1/users/42        → Partial update user 42
DELETE /api/v1/users/42        → Delete user 42

# Sub-resources (relationships)
GET    /api/v1/users/42/posts        → User 42's posts
POST   /api/v1/users/42/posts        → Create a post for user 42
GET    /api/v1/users/42/posts/7      → User 42's post 7

# Query parameters (filtering, sorting, pagination)
GET    /api/v1/users?role=admin&sort=-createdAt&page=2&limit=20
GET    /api/v1/posts?search=nodejs&tags=tutorial,guide

Response format (consistent envelope):

{
  "success": true,
  "data": { ... },
  "meta": { "total": 100, "page": 1, "limit": 20 },
  "links": { "next": "/api/v1/users?page=2" }
}

Status code usage:

Code When to use
200 Successful GET, PUT, PATCH
201 Successful POST (resource created)
204 Successful DELETE (no content)
400 Bad request (validation errors)
401 Unauthorized (no/invalid auth)
403 Forbidden (auth OK, no permission)
404 Resource not found
409 Conflict (duplicate email, etc.)
422 Unprocessable entity (semantic validation)
429 Too many requests
500 Internal server error

🏠 Real-world analogy: A REST API is like a well-organized library catalog. Each book (resource) has a unique call number (URL). You can browse (GET), donate (POST), update descriptions (PUT/PATCH), or withdraw (DELETE). The catalog follows a consistent system so anyone can find what they need.

💻 Code Example

codeTap to expand ⛶
1// REST API Design — Production-Grade Patterns
2
3const express = require("express");
4const app = express();
5app.use(express.json());
6
7// === Response helper ===
8class APIResponse {
9 static success(res, data, statusCode = 200) {
10 res.status(statusCode).json({ success: true, data });
11 }
12
13 static created(res, data) {
14 res.status(201).json({ success: true, data });
15 }
16
17 static noContent(res) {
18 res.status(204).end();
19 }
20
21 static paginated(res, data, meta) {
22 res.json({ success: true, data, meta });
23 }
24
25 static error(res, message, statusCode = 500, details = null) {
26 const response = { success: false, error: { message, statusCode } };
27 if (details) response.error.details = details;
28 res.status(statusCode).json(response);
29 }
30}
31
32// === Query parameter helpers ===
33function parsePagination(query) {
34 const page = Math.max(1, parseInt(query.page) || 1);
35 const limit = Math.min(100, Math.max(1, parseInt(query.limit) || 20));
36 const skip = (page - 1) * limit;
37 return { page, limit, skip };
38}
39
40function parseSort(sortString) {
41 if (!sortString) return { createdAt: -1 }; // Default: newest first
42 const sort = {};
43 sortString.split(",").forEach((field) => {
44 if (field.startsWith("-")) {
45 sort[field.substring(1)] = -1; // Descending
46 } else {
47 sort[field] = 1; // Ascending
48 }
49 });
50 return sort;
51}
52
53function parseFilters(query, allowedFields) {
54 const filters = {};
55 for (const field of allowedFields) {
56 if (query[field] !== undefined) {
57 filters[field] = query[field];
58 }
59 }
60 return filters;
61}
62
63// === RESTful routes with best practices ===
64
65// Mock data
66let posts = [
67 { id: 1, title: "Node.js Basics", content: "Learn Node.js...", authorId: 1, tags: ["nodejs", "tutorial"], published: true, createdAt: new Date("2024-01-15") },
68 { id: 2, title: "Express Guide", content: "Master Express...", authorId: 1, tags: ["express"], published: true, createdAt: new Date("2024-02-20") },
69 { id: 3, title: "Draft Post", content: "WIP...", authorId: 2, tags: [], published: false, createdAt: new Date("2024-03-01") },
70];
71let nextId = 4;
72
73// GET /api/v1/posts — List with filtering, sorting, pagination
74app.get("/api/v1/posts", (req, res) => {
75 const { page, limit, skip } = parsePagination(req.query);
76 const sort = parseSort(req.query.sort);
77 const filters = parseFilters(req.query, ["published", "authorId"]);
78
79 let result = [...posts];
80
81 // Apply filters
82 if (filters.published !== undefined) {
83 result = result.filter((p) => p.published === (filters.published === "true"));
84 }
85 if (filters.authorId) {
86 result = result.filter((p) => p.authorId === parseInt(filters.authorId));
87 }
88
89 // Search
90 if (req.query.search) {
91 const search = req.query.search.toLowerCase();
92 result = result.filter(
93 (p) => p.title.toLowerCase().includes(search) || p.content.toLowerCase().includes(search)
94 );
95 }
96
97 const total = result.length;
98 const paginated = result.slice(skip, skip + limit);
99
100 APIResponse.paginated(res, paginated, {
101 total,
102 page,
103 limit,
104 totalPages: Math.ceil(total / limit),
105 hasNext: skip + limit < total,
106 hasPrev: page > 1,
107 });
108});
109
110// GET /api/v1/posts/:id — Get single resource
111app.get("/api/v1/posts/:id", (req, res) => {
112 const post = posts.find((p) => p.id === parseInt(req.params.id));
113 if (!post) return APIResponse.error(res, "Post not found", 404);
114 APIResponse.success(res, post);
115});
116
117// POST /api/v1/posts — Create resource
118app.post("/api/v1/posts", (req, res) => {
119 const { title, content, tags = [], published = false } = req.body;
120
121 // Validation
122 const errors = [];
123 if (!title || title.length < 3) errors.push({ field: "title", message: "Title required (min 3 chars)" });
124 if (!content) errors.push({ field: "content", message: "Content required" });
125 if (errors.length) return APIResponse.error(res, "Validation failed", 400, errors);
126
127 const post = { id: nextId++, title, content, tags, published, authorId: 1, createdAt: new Date() };
128 posts.push(post);
129
130 APIResponse.created(res, post);
131});
132
133// PUT /api/v1/posts/:id — Full update
134app.put("/api/v1/posts/:id", (req, res) => {
135 const index = posts.findIndex((p) => p.id === parseInt(req.params.id));
136 if (index === -1) return APIResponse.error(res, "Post not found", 404);
137
138 const { title, content, tags, published } = req.body;
139 if (!title || !content) return APIResponse.error(res, "Title and content required for PUT", 400);
140
141 posts[index] = { ...posts[index], title, content, tags: tags || [], published: published || false };
142 APIResponse.success(res, posts[index]);
143});
144
145// PATCH /api/v1/posts/:id — Partial update
146app.patch("/api/v1/posts/:id", (req, res) => {
147 const index = posts.findIndex((p) => p.id === parseInt(req.params.id));
148 if (index === -1) return APIResponse.error(res, "Post not found", 404);
149
150 posts[index] = { ...posts[index], ...req.body, id: posts[index].id }; // Don't allow ID change
151 APIResponse.success(res, posts[index]);
152});
153
154// DELETE /api/v1/posts/:id — Delete resource
155app.delete("/api/v1/posts/:id", (req, res) => {
156 const index = posts.findIndex((p) => p.id === parseInt(req.params.id));
157 if (index === -1) return APIResponse.error(res, "Post not found", 404);
158 posts.splice(index, 1);
159 APIResponse.noContent(res);
160});
161
162app.listen(3000, () => console.log("REST API on http://localhost:3000"));

🏋️ Practice Exercise

Exercises:

  1. Build a complete REST API for a blog with posts, comments, and tags — follow all conventions
  2. Implement filtering, sorting, searching, and cursor-based pagination
  3. Add proper validation with detailed error messages that include field names and reasons
  4. Implement API versioning using URL prefix (/api/v1/, /api/v2/)
  5. Create a consistent response envelope ({ success, data, meta, error }) used across all endpoints
  6. Add HATEOAS links to responses (e.g., links: { self, next, prev, author })

⚠️ Common Mistakes

  • Using verbs in URLs — /api/getUsers is wrong; use nouns: /api/users with the HTTP method conveying the action

  • Not using proper HTTP status codes — returning 200 for everything makes error handling impossible for API consumers

  • Inconsistent response formats — some endpoints return { data }, others { result }, others raw arrays; use a consistent envelope

  • Not supporting pagination by default — returning all records in a collection crashes when data grows; always paginate

  • Using PUT when PATCH is more appropriate — PUT replaces the entire resource; PATCH updates only the provided fields

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for REST API Design Principles. Login to unlock this feature.