Node.js: REST API Design

Last updated: 2026-08-26

Alice's team is developing both the front end and back end for a task management system. The front-end developers are complaining that they don't know which APIs are available, which HTTP methods to use, or what formats the responses will be in. The back-end developers are just as frustrated—for the same "update task" operation, some use POST, some use PUT, and others use PATCH, resulting in a wide variety of response formats. Collaboration has descended into chaos.

Alice decided to adopt the REST specification. After the team standardized resource naming, method mapping, status codes, and response formats, the API became clear and predictable; the front-end team no longer had to repeatedly check the API documentation, and collaboration efficiency doubled.

1. What You'll Learn



2. REST Architecture Principles

(1) What is REST?

REST (Representational State Transfer) is a software architectural style proposed by Roy Fielding in 2000. It defines a set of constraints for designing interfaces for web applications. REST is not a protocol or a standard, but rather a design philosophy.

(2) Four Core Principles

Principle Meaning Example
Resource Everything is a resource, identified by a URL /tasks, /users/42
Representation Layer The format in which a resource is represented, such as JSON {"id": 1, "title": "Learn REST"}
Stateless Each request contains all necessary information Requests carry a token and do not rely on sessions
Uniform Interface Operate on resources using standard HTTP methods GET to read, POST to create, DELETE to delete

▶ Example: Stateless vs. Stateful

JAVASCRIPT
// Stateful: Depends on server session (not RESTful)
app.post('/login', (req, res) => {
  req.session.userId = 42; // Server State Saving
  res.send('logged in');
});

app.get('/profile', (req, res) => {
  const userId = req.session.userId; // Depends on the server status
  res.json({ id: userId, name: 'Alice' });
});

// Stateless:Include authentication information with every request(RESTful)
app.get('/profile', (req, res) => {
  const userId = verifyToken(req.headers.authorization);
  res.json({ id: userId, name: 'Alice' });
});
▶ Try it Yourself

3. CRUD and HTTP Method Mappings

(1) Standard mapping relationship

The core idea of REST is to use HTTP methods to express the intent of operations on resources, rather than embedding action verbs in URLs.

CRUD Operations HTTP Methods Paths Idempotency Security
Create POST /tasks No No
Read (List) GET /tasks Yes Yes
Read (Single) GET /tasks/42 Yes Yes
Update (Full) PUT /tasks/42 Yes No
Update (partial) PATCH /tasks/42 No No
Delete DELETE /tasks/42 Yes No

(2) A Detailed Explanation of Idempotency

Idempotence means that executing the same request once has the same effect as executing it multiple times. GET, PUT, and DELETE are idempotent, while POST and PATCH are not.

▶ Example: Differences in Idempotency Between PUT and POST

JAVASCRIPT
// POST: Creates a new resource on every call (Non-idempotent)
// 1st POST /tasks → Create id=1
// 2nd POST /tasks → Create id=2
app.post('/tasks', (req, res) => {
  const task = { id: nextId++, ...req.body };
  tasks.push(task);
  res.status(201).json(task);
});

// PUT: Replaces the same resource on every call (Idempotent)
// 1st PUT /tasks/1 → Replace id=1
// 2nd PUT /tasks/1 → Replace id=1 (The results are the same)
app.put('/tasks/:id', (req, res) => {
  const idx = tasks.findIndex(t => t.id === parseInt(req.params.id));
  if (idx === -1) return res.status(404).json({ error: 'Not found' });
  tasks[idx] = { id: parseInt(req.params.id), ...req.body };
  res.json(tasks[idx]);
});
▶ Try it Yourself

▶ Example: PATCH Partial Update

JAVASCRIPT
// PATCH:Modify only the fields provided
app.patch('/tasks/:id', (req, res) => {
  const task = tasks.find(t => t.id === parseInt(req.params.id));
  if (!task) return res.status(404).json({ error: 'Not found' });
  Object.assign(task, req.body);
  res.json(task);
});

// Request:Modify only status Field
// PATCH /tasks/1  {"status": "done"}
// Raw Data:{"id":1,"title":"Learn REST","status":"pending"}
// Results:{"id":1,"title":"Learn REST","status":"done"}
▶ Try it Yourself

4. URL Design Guidelines

(1) Core Rules

RESTful URL design follows a set of conventions that make APIs intuitive and easy to read.

Rule Correct ✅ Incorrect ❌
Use nouns, not verbs GET /tasks GET /getTasks
Use the plural, not the singular /tasks /task
Representing Relationships with Nesting /users/42/tasks /tasksByUser?userId=42
No more than 3 levels /users/42/tasks/1 /orgs/1/teams/2/users/42/tasks
Filter by query parameters /tasks?status=done /doneTasks
Using kebab-case /task-item /taskItems

(2) Design of Nested Resources

Nested resources indicate a hierarchical relationship. Use nested paths when a child resource cannot exist independently of its parent resource.

▶ Example: URL Design for a Task Management System

TEXT 📖 Display only
# Mission Resources
GET    /tasks              # Get the task list
POST   /tasks              # Create a New Task
GET    /tasks/42           # Get a Single Task
PUT    /tasks/42           # Full Update Task
PATCH  /tasks/42           # Partial Update Task
DELETE /tasks/42           # Delete Task

# Comments on the Assignment(Nested Resources)
GET    /tasks/42/comments           # Get a Task42List of comments
POST   /tasks/42/comments           # For the mission42Add a comment
GET    /tasks/42/comments/7         # Get a Task42Comments on7
DELETE /tasks/42/comments/7         # Delete Comment7

# Filtering and Pagination
GET    /tasks?status=done&page=2&limit=20
GET    /tasks?sort=-created_at      # Sort by creation date in reverse chronological order

▶ Example: Common Uses of URL Query Parameters

JAVASCRIPT
app.get('/tasks', (req, res) => {
  let result = [...tasks];

  // Filter
  if (req.query.status) {
    result = result.filter(t => t.status === req.query.status);
  }

  // Sort
  if (req.query.sort) {
    const field = req.query.sort.startsWith('-')
      ? req.query.sort.slice(1)
      : req.query.sort;
    const order = req.query.sort.startsWith('-') ? -1 : 1;
    result.sort((a, b) => (a[field] > b[field] ? order : -order));
  }

  // Pagination
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 20;
  const start = (page - 1) * limit;
  result = result.slice(start, start + limit);

  res.json({
    data: result,
    page,
    limit,
    total: tasks.length
  });
});
▶ Try it Yourself

5. Choosing HTTP Status Codes

(1) Classification and Selection of Status Codes

HTTP status codes are key signals for communication between REST APIs and clients. By choosing the correct status code, clients can accurately understand the result of a request.

Scenario Status Code Meaning Description
Resource retrieved successfully 200 OK Request successful Returned when GET/PUT/PATCH is successful
Resource created successfully 201 Created Resource created Returned upon successful POST; should include a Location header
Resource successfully deleted 204 No Content No content Returned when DELETE is successful; no response body
Invalid request parameters 400 Bad Request Client request syntax error Missing required fields, invalid format
Not authenticated 401 Unauthorized No authentication information provided Missing or invalid token
No Permission 403 Forbidden Authenticated but No Permission Regular User Accessing Admin Interface
Resource does not exist 404 Not Found The requested resource does not exist The resource with this ID was not found
Server Error 500 Internal Server Error Internal Server Error Uncaught Exception

(2) Common Mistakes: Incorrect Use of Status Codes

▶ Example: Correct Use of Status Codes

JAVASCRIPT
// Create a Resource → 201 + Location
app.post('/tasks', (req, res) => {
  const task = { id: nextId++, ...req.body };
  tasks.push(task);
  res.status(201)
     .location(`/tasks/${task.id}`)
     .json(task);
});

// Delete Resource → 204(Non-responsive body)
app.delete('/tasks/:id', (req, res) => {
  const idx = tasks.findIndex(t => t.id === parseInt(req.params.id));
  if (idx === -1) return res.status(404).json({ error: 'Not found' });
  tasks.splice(idx, 1);
  res.status(204).end();
});

// Verification Failed → 400 + Error Details
app.post('/tasks', (req, res) => {
  if (!req.body.title) {
    return res.status(400).json({
      error: 'Validation failed',
      details: [{ field: 'title', message: 'Title is required' }]
    });
  }
  // ...
});
▶ Try it Yourself

6. Request and Response Formats

(1) JSON Specification Conventions

Convention Standard Example
Field naming camelCase createdAt, taskId
Date Format ISO 8601 2025-07-03T10:30:00Z
List Response Includes data + pagination information {"data": [...], "total": 100}
Error Response Includes error + details {"error": "Not found", "details": [...]}
Handling Null Values Use null instead of omitting the field {"description": null}
ID Type String (to avoid precision issues) {"id": "42"}

▶ Example: Standardized list response

JAVASCRIPT
app.get('/tasks', (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 20;
  const start = (page - 1) * limit;
  const data = tasks.slice(start, start + limit);

  res.json({
    data,
    pagination: {
      page,
      limit,
      total: tasks.length,
      totalPages: Math.ceil(tasks.length / limit)
    }
  });
});
▶ Try it Yourself
TEXT 📖 Display only
// Response Example
{
  "data": [
    {
      "id": "1",
      "title": "Learn REST",
      "status": "pending",
      "createdAt": "2025-07-03T10:30:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "totalPages": 1
  }
}

▶ Example: Standardized Error Response

JAVASCRIPT
// Unified Error Handling Middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: err.message || 'Internal Server Error',
    details: err.details || [],
    requestId: req.id,
    timestamp: new Date().toISOString()
  });
});

// Custom Error Classes
class ApiError extends Error {
  constructor(status, message, details = []) {
    super(message);
    this.status = status;
    this.details = details;
  }
}

// Usage
app.get('/tasks/:id', (req, res, next) => {
  const task = tasks.find(t => t.id === parseInt(req.params.id));
  if (!task) {
    return next(new ApiError(404, 'Task not found', [
      { field: 'id', message: `No task with id ${req.params.id}` }
    ]));
  }
  res.json(task);
});
▶ Try it Yourself
TEXT 📖 Display only
// Examples of Error Responses
{
  "error": "Task not found",
  "details": [
    { "field": "id", "message": "No task with id 999" }
  ],
  "requestId": "req-a1b2c3",
  "timestamp": "2025-07-03T10:30:00Z"
}


7. API Versioning Strategy

(1) Comparison of Three Mainstream Strategies

Strategy Example Advantages Disadvantages Applicable Scenarios
URL Path /api/v1/tasks Intuitive, can be tested in a browser Longer URLs, controversial among purist REST advocates Most public APIs
Request Header Accept: application/vnd.myapi.v1+json Clean, pure RESTful URL Not intuitive, difficult to debug Pursues REST purity
Query Parameter /api/tasks?version=1 Simplest Easily overlooked; complex caching strategy Internal APIs, simple projects

(2) Best Practices for Versioning

▶ Example: Implementing URL Path Versioning

JAVASCRIPT
// Routing Structure
// /api/v1/tasks → v1 Logic
// /api/v2/tasks → v2 Logic

const express = require('express');
const app = express();

// v1 Routing
const v1Router = express.Router();
v1Router.get('/tasks', (req, res) => {
  res.json({ data: tasks, version: 'v1' }); // v1 Return Format
});

// v2 Routing(Response Format Upgrade)
const v2Router = express.Router();
v2Router.get('/tasks', (req, res) => {
  res.json({                        // v2 Return Format(Includes pagination)
    data: tasks,
    pagination: { page: 1, total: tasks.length }
  });
});

app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

// Response Header Version
app.use('/api/v2', (req, res, next) => {
  res.setHeader('X-API-Version', '2.0');
  next();
});
▶ Try it Yourself

8. REST Maturity Model

(1) Richardson's Maturity Model

Leonard Richardson proposed a model for measuring the RESTful maturity of an API:

100%
graph TD
    L0["Level 0: Single endpoint<br/>HTTP as a tunnel<br/>e.g. POST /api  {action: getTasks}"]
    L1["Level 1: Resource URLs<br/>One URL per resource<br/>e.g. POST /tasks, POST /users"]
    L2["Level 2: HTTP Methods<br/>GET/POST/PUT/DELETE<br/>e.g. GET /tasks, DELETE /tasks/1"]
    L3["Level 3: HATEOAS<br/>Responses contain hyperlinks<br/>e.g. Response includes next, self links"]
    L0 --> L1 --> L2 --> L3
    style L0 fill:#ff6b6b,color:#fff
    style L1 fill:#ffa502,color:#fff
    style L2 fill:#2ed573,color:#fff
    style L3 fill:#1e90ff,color:#fff
Level Characteristics Sample Request Sample Response
Level 0 HTTP tunnel, single URL POST /api {"action":"getTasks"} {"tasks": [...]}
Level 1 Resource separation; any method allowed POST /tasks {"tasks": [...]}
Level 2 Semantically Correct HTTP GET /tasks 200 {"data": [...]}
Level 3 HATEOAS Hypermedia GET /tasks/1 Includes _links Navigation

(2) A Detailed Explanation of HATEOAS

HATEOAS (Hypermedia as the Engine of Application State) requires that responses include links to relevant operations, so clients do not need to hard-code URLs.

JAVASCRIPT
app.get('/tasks/:id', (req, res) => {
  const task = tasks.find(t => t.id === parseInt(req.params.id));
  if (!task) return res.status(404).json({ error: 'Not found' });

  res.json({
    ...task,
    _links: {
      self: { href: `/tasks/${task.id}`, method: 'GET' },
      update: { href: `/tasks/${task.id}`, method: 'PUT' },
      delete: { href: `/tasks/${task.id}`, method: 'DELETE' },
      assign: { href: `/tasks/${task.id}/assignee`, method: 'POST' },
      comments: { href: `/tasks/${task.id}/comments`, method: 'GET' }
    }
  });
});
▶ Try it Yourself
TEXT 📖 Display only
// Response
{
  "id": "1",
  "title": "Learn REST",
  "status": "pending",
  "createdAt": "2025-07-03T10:30:00Z",
  "_links": {
    "self": { "href": "/tasks/1", "method": "GET" },
    "update": { "href": "/tasks/1", "method": "PUT" },
    "delete": { "href": "/tasks/1", "method": "DELETE" },
    "assign": { "href": "/tasks/1/assignee", "method": "POST" },
    "comments": { "href": "/tasks/1/comments", "method": "GET" }
  }
}


9. Comprehensive Example: Complete Design of the Task Management API

Alice's team designed a complete RESTful API for the task management system, covering everything from resource definitions to error handling.

▶ Example: Complete Task Management API

(1) Resource Definition

Resource Path Description
Task Collection /api/v1/tasks All Tasks
Single Task /api/v1/tasks/:id Specified Task
Task Comments /api/v1/tasks/:id/comments Comments on a Specific Task
Task Tag /api/v1/tasks/:id/tags Tag for a specific task

(2) Method Mapping and Request/Response

JAVASCRIPT 📖 Display only
const express = require('express');
const app = express();
app.use(express.json());

let tasks = [
  { id: 1, title: 'Design database schema', status: 'done', priority: 'high', createdAt: '2025-07-01T08:00:00Z' },
  { id: 2, title: 'Implement REST API', status: 'in-progress', priority: 'high', createdAt: '2025-07-02T09:00:00Z' }
];
let nextId = 3;

// GET /api/v1/tasks — Get the task list
app.get('/api/v1/tasks', (req, res) => {
  const { status, priority, page = 1, limit = 20 } = req.query;
  let result = [...tasks];
  if (status) result = result.filter(t => t.status === status);
  if (priority) result = result.filter(t => t.priority === priority);

  const start = (page - 1) * limit;
  const data = result.slice(start, start + Number(limit));

  res.json({
    data,
    pagination: {
      page: Number(page),
      limit: Number(limit),
      total: result.length,
      totalPages: Math.ceil(result.length / Number(limit))
    }
  });
});

// GET /api/v1/tasks/:id — Get a Single Task
app.get('/api/v1/tasks/:id', (req, res) => {
  const task = tasks.find(t => t.id === parseInt(req.params.id));
  if (!task) {
    return res.status(404).json({
      error: 'Task not found',
      details: [{ field: 'id', message: `No task with id ${req.params.id}` }],
      timestamp: new Date().toISOString()
    });
  }
  res.json({
    data: task,
    _links: {
      self: { href: `/api/v1/tasks/${task.id}` },
      update: { href: `/api/v1/tasks/${task.id}`, method: 'PUT' },
      delete: { href: `/api/v1/tasks/${task.id}`, method: 'DELETE' },
      comments: { href: `/api/v1/tasks/${task.id}/comments` }
    }
  });
});

// POST /api/v1/tasks — Create a Task
app.post('/api/v1/tasks', (req, res) => {
  const { title, priority } = req.body;
  if (!title) {
    return res.status(400).json({
      error: 'Validation failed',
      details: [{ field: 'title', message: 'Title is required' }],
      timestamp: new Date().toISOString()
    });
  }
  const task = {
    id: nextId++,
    title,
    status: 'pending',
    priority: priority || 'medium',
    createdAt: new Date().toISOString()
  };
  tasks.push(task);
  res.status(201).location(`/api/v1/tasks/${task.id}`).json({ data: task });
});

// PUT /api/v1/tasks/:id — Full Update
app.put('/api/v1/tasks/:id', (req, res) => {
  const idx = tasks.findIndex(t => t.id === parseInt(req.params.id));
  if (idx === -1) {
    return res.status(404).json({
      error: 'Task not found',
      details: [{ field: 'id', message: `No task with id ${req.params.id}` }],
      timestamp: new Date().toISOString()
    });
  }
  const { title, status, priority } = req.body;
  if (!title || !status) {
    return res.status(400).json({
      error: 'Validation failed',
      details: [
        ...(!title ? [{ field: 'title', message: 'Title is required' }] : []),
        ...(!status ? [{ field: 'status', message: 'Status is required' }] : [])
      ],
      timestamp: new Date().toISOString()
    });
  }
  tasks[idx] = { id: tasks[idx].id, title, status, priority: priority || 'medium', createdAt: tasks[idx].createdAt };
  res.json({ data: tasks[idx] });
});

// PATCH /api/v1/tasks/:id — Partial Update
app.patch('/api/v1/tasks/:id', (req, res) => {
  const task = tasks.find(t => t.id === parseInt(req.params.id));
  if (!task) {
    return res.status(404).json({
      error: 'Task not found',
      details: [{ field: 'id', message: `No task with id ${req.params.id}` }],
      timestamp: new Date().toISOString()
    });
  }
  Object.assign(task, req.body);
  res.json({ data: task });
});

// DELETE /api/v1/tasks/:id — Delete Task
app.delete('/api/v1/tasks/:id', (req, res) => {
  const idx = tasks.findIndex(t => t.id === parseInt(req.params.id));
  if (idx === -1) {
    return res.status(404).json({
      error: 'Task not found',
      details: [{ field: 'id', message: `No task with id ${req.params.id}` }],
      timestamp: new Date().toISOString()
    });
  }
  tasks.splice(idx, 1);
  res.status(204).end();
});

app.listen(3000, () => console.log('Task API running on port 3000'));
111 logic lines (exceeds 40-line limit, display only)

(3) Quick Reference for Requests and Responses

BASH
# Create a Task
curl -X POST http://localhost:3000/api/v1/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Write documentation","priority":"high"}'

# Get the task list(Filter + Pagination)
curl http://localhost:3000/api/v1/tasks?status=pending&page=1&limit=10

# Partial Update
curl -X PATCH http://localhost:3000/api/v1/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"status":"done"}'

# Delete Task
curl -X DELETE http://localhost:3000/api/v1/tasks/2
TEXT 📖 Display only
// POST Created successfully → 201
Status: 201 Created
Location: /api/v1/tasks/3
{ "data": { "id": 3, "title": "Write documentation", "status": "pending", "priority": "high", "createdAt": "2025-07-03T10:30:00Z" } }

// PATCH Update successful → 200
{ "data": { "id": 1, "title": "Design database schema", "status": "done", "priority": "high", "createdAt": "2025-07-01T08:00:00Z" } }

// DELETE Success → 204
Status: 204 No Content
(empty body)

// 404 Error
{ "error": "Task not found", "details": [{ "field": "id", "message": "No task with id 999" }], "timestamp": "2025-07-03T10:30:00Z" }

// 400 Validation Error
{ "error": "Validation failed", "details": [{ "field": "title", "message": "Title is required" }], "timestamp": "2025-07-03T10:30:00Z" }

❓ FAQ

Q What is the difference between REST and GraphQL?
A REST is resource-based; each URL corresponds to a resource, which is manipulated using HTTP methods. GraphQL is based on a query language; clients retrieve fields on demand from a single endpoint. REST is suitable for CRUD scenarios with well-defined resources, while GraphQL is suitable for complex relational queries.
Q What is the difference between PUT and PATCH?
A PUT performs a full replacement; you must provide all fields of the resource, and any missing fields will be set to their default values. PATCH performs a partial update; it modifies only the fields you provide, while fields not provided remain unchanged. PUT is idempotent, while PATCH is not guaranteed to be idempotent.
Q What’s the best way to version an API?
A URL path versioning (/api/v1/) is the most intuitive; it can be tested directly in a browser and is the choice for most public APIs. Request header versioning is more RESTful but more complicated to debug. Query parameters are the simplest but are easily overlooked. URL path versioning is recommended for beginners.
Q Does a REST API have to return JSON?
A Not necessarily. REST does not restrict the format; you can use XML, HTML, JSON, and others. However, JSON is currently the most commonly used format because it is lightweight, easy to parse, and natively compatible with JavaScript. You can negotiate the format using the Content-Type header.
Q What is idempotence?
A Idempotence means that executing the same request once produces the same result as executing it multiple times. GET is idempotent (multiple reads yield the same result), PUT is idempotent (multiple replacements yield the same result), DELETE is idempotent (deleting a resource that has already been deleted still returns a success), and POST is not idempotent (it creates a new resource each time).
Q What should I do if the resource hierarchy is too deep?
A If the hierarchy exceeds two levels of nesting, consider promoting the child resources to top-level resources and linking them using query parameters. For example, if /users/42/tasks/1/comments/5 is too deep, you can change it to /comments/5 or /tasks/1/comments/5.
Q How does a REST API handle bulk operations?
A There is no standard approach in REST. Common practices include: using a POST request /tasks/batch with an array to create records; using a PATCH request /tasks with an array to update records in bulk; and using a DELETE request /tasks?ids=1,2,3 to delete records in bulk. Custom endpoints must be clearly documented.

📖 Summary


📝 Exercises

  1. Complete all the code examples in this lesson and make sure each one runs correctly.
  2. Modify the comprehensive example and add your own extensions
  3. Review the official documentation, identify 1–2 APIs not covered in this lesson, and write test code for them.
  4. Reflection: How would you apply what you’ve learned in this lesson to a real-world project?
  5. Try to combine what you’ve learned in this lesson with material from previous lessons to build a small project.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏