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:
- Resources — Everything is a resource identified by a URL
- HTTP methods — Use verbs correctly (GET, POST, PUT, PATCH, DELETE)
- Stateless — Each request contains all information needed
- Uniform interface — Consistent URL patterns and response formats
- 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
1// REST API Design — Production-Grade Patterns23const express = require("express");4const app = express();5app.use(express.json());67// === Response helper ===8class APIResponse {9 static success(res, data, statusCode = 200) {10 res.status(statusCode).json({ success: true, data });11 }1213 static created(res, data) {14 res.status(201).json({ success: true, data });15 }1617 static noContent(res) {18 res.status(204).end();19 }2021 static paginated(res, data, meta) {22 res.json({ success: true, data, meta });23 }2425 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}3132// === 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}3940function parseSort(sortString) {41 if (!sortString) return { createdAt: -1 }; // Default: newest first42 const sort = {};43 sortString.split(",").forEach((field) => {44 if (field.startsWith("-")) {45 sort[field.substring(1)] = -1; // Descending46 } else {47 sort[field] = 1; // Ascending48 }49 });50 return sort;51}5253function 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}6263// === RESTful routes with best practices ===6465// Mock data66let 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;7273// GET /api/v1/posts — List with filtering, sorting, pagination74app.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"]);7879 let result = [...posts];8081 // Apply filters82 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 }8889 // Search90 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 }9697 const total = result.length;98 const paginated = result.slice(skip, skip + limit);99100 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});109110// GET /api/v1/posts/:id — Get single resource111app.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});116117// POST /api/v1/posts — Create resource118app.post("/api/v1/posts", (req, res) => {119 const { title, content, tags = [], published = false } = req.body;120121 // Validation122 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);126127 const post = { id: nextId++, title, content, tags, published, authorId: 1, createdAt: new Date() };128 posts.push(post);129130 APIResponse.created(res, post);131});132133// PUT /api/v1/posts/:id — Full update134app.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);137138 const { title, content, tags, published } = req.body;139 if (!title || !content) return APIResponse.error(res, "Title and content required for PUT", 400);140141 posts[index] = { ...posts[index], title, content, tags: tags || [], published: published || false };142 APIResponse.success(res, posts[index]);143});144145// PATCH /api/v1/posts/:id — Partial update146app.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);149150 posts[index] = { ...posts[index], ...req.body, id: posts[index].id }; // Don't allow ID change151 APIResponse.success(res, posts[index]);152});153154// DELETE /api/v1/posts/:id — Delete resource155app.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});161162app.listen(3000, () => console.log("REST API on http://localhost:3000"));
🏋️ Practice Exercise
Exercises:
- Build a complete REST API for a blog with posts, comments, and tags — follow all conventions
- Implement filtering, sorting, searching, and cursor-based pagination
- Add proper validation with detailed error messages that include field names and reasons
- Implement API versioning using URL prefix (
/api/v1/,/api/v2/) - Create a consistent response envelope (
{ success, data, meta, error }) used across all endpoints - Add HATEOAS links to responses (e.g.,
links: { self, next, prev, author })
⚠️ Common Mistakes
Using verbs in URLs —
/api/getUsersis wrong; use nouns:/api/userswith the HTTP method conveying the actionNot 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 envelopeNot 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.