Node.js: RESTful API 项目

最后更新:2026-08-26

1. 项目背景

Alice 和 Bob 接到一个任务:为一家在线书店开发书籍管理 API。Alice 负责设计路由和中间件架构,Bob 负责编写业务逻辑。两人约定统一响应格式和错误处理规范,2 小时内完成了从项目搭建到完整 CRUD 的全部功能。

(1) 你将学到



2. 项目目录结构设计

合理的目录结构是项目可维护性的基础。Alice 参考社区最佳实践,将项目按职责分层:

TEXT 📖 仅展示
book-api/
├── app.js
├── routes/
│   └── books.js
├── controllers/
│   └── bookController.js
├── middleware/
│   ├── validate.js
│   └── errorHandler.js
└── models/
    └── bookModel.js
目录/文件 职责 说明
app.js 入口文件 创建 Express 实例,挂载路由与中间件
routes/ 路由定义 定义 HTTP 方法与路径,指向对应 controller
controllers/ 业务逻辑 处理请求,调用 model,返回响应
middleware/ 中间件 请求验证、错误处理等横切关注点
models/ 数据模型 数据存储与数据操作封装


3. API 端点设计

Bob 根据业务需求,整理出所有 API 端点:

HTTP 方法 路径 功能 成功状态码 失败状态码
GET /api/books 获取所有书籍 200
GET /api/books/:id 获取单本书籍 200 404
POST /api/books 新增书籍 201 400
PUT /api/books/:id 更新书籍 200 404 / 400
DELETE /api/books/:id 删除书籍 200 404


4. 统一响应格式

Alice 坚持统一响应格式,让前端团队不用猜字段名:

▶ 示例:成功响应

JAVASCRIPT
{
  "success": true,
  "data": { "id": 1, "title": "Node.js Guide", "author": "Alice" }
}
▶ 试一试

▶ 示例:错误响应

JAVASCRIPT
{
  "success": false,
  "error": { "code": 404, "message": "Book not found" }
}
▶ 试一试
字段 类型 说明
success boolean 请求是否成功
data any 成功时返回的数据
error.code number 错误状态码
error.message string 错误描述信息


5. 数据模型层

Bob 在 models 目录下用数组模拟数据库,封装所有数据操作:

▶ 示例:bookModel.js

JAVASCRIPT
const books = [
  { id: 1, title: "Node.js Guide", author: "Alice", year: 2024 }
];

let nextId = 2;

function findAll() {
  return books;
}

function findById(id) {
  return books.find(b => b.id === id);
}

function create(data) {
  const book = { id: nextId++, ...data };
  books.push(book);
  return book;
}

function update(id, data) {
  const index = books.findIndex(b => b.id === id);
  if (index === -1) return null;
  books[index] = { ...books[index], ...data };
  return books[index];
}

function remove(id) {
  const index = books.findIndex(b => b.id === id);
  if (index === -1) return false;
  books.splice(index, 1);
  return true;
}

module.exports = { findAll, findById, create, update, remove };
▶ 试一试

6. 请求验证中间件

Alice 在中间件层实现数据验证,确保无效请求不会到达 controller:

▶ 示例:validate.js

JAVASCRIPT
function validateBook(req, res, next) {
  const { title, author, year } = req.body;
  const errors = [];
  if (!title || typeof title !== "string") {
    errors.push("title is required and must be a string");
  }
  if (!author || typeof author !== "string") {
    errors.push("author is required and must be a string");
  }
  if (year !== undefined && (typeof year !== "number" || year < 0)) {
    errors.push("year must be a non-negative number");
  }
  if (errors.length > 0) {
    return res.status(400).json({
      success: false,
      error: { code: 400, message: errors.join("; ") }
    });
  }
  next();
}

module.exports = { validateBook };
▶ 试一试

7. 错误处理中间件

▶ 示例:errorHandler.js

JAVASCRIPT
function errorHandler(err, req, res, next) {
  console.error(err.stack);
  const status = err.status || 500;
  res.status(status).json({
    success: false,
    error: { code: status, message: err.message || "Internal Server Error" }
  });
}

function createError(status, message) {
  const err = new Error(message);
  err.status = status;
  return err;
}

module.exports = { errorHandler, createError };
▶ 试一试

8. 控制器层

Bob 在 controller 中处理业务逻辑,调用 model 并返回统一格式响应:

▶ 示例:bookController.js

JAVASCRIPT
const Book = require("../models/bookModel");
const { createError } = require("../middleware/errorHandler");

function getAllBooks(req, res) {
  res.json({ success: true, data: Book.findAll() });
}

function getBookById(req, res, next) {
  const book = Book.findById(Number(req.params.id));
  if (!book) return next(createError(404, "Book not found"));
  res.json({ success: true, data: book });
}

function createBook(req, res) {
  const book = Book.create(req.body);
  res.status(201).json({ success: true, data: book });
}

function updateBook(req, res, next) {
  const book = Book.update(Number(req.params.id), req.body);
  if (!book) return next(createError(404, "Book not found"));
  res.json({ success: true, data: book });
}

function deleteBook(req, res, next) {
  const removed = Book.remove(Number(req.params.id));
  if (!removed) return next(createError(404, "Book not found"));
  res.json({ success: true, data: { message: "Book deleted" } });
}

module.exports = { getAllBooks, getBookById, createBook, updateBook, deleteBook };
▶ 试一试

9. 路由定义

Alice 将路由与 controller 解耦,路由只负责映射:

▶ 示例:routes/books.js

JAVASCRIPT
const express = require("express");
const router = express.Router();
const controller = require("../controllers/bookController");
const { validateBook } = require("../middleware/validate");

router.get("/", controller.getAllBooks);
router.get("/:id", controller.getBookById);
router.post("/", validateBook, controller.createBook);
router.put("/:id", validateBook, controller.updateBook);
router.delete("/:id", controller.deleteBook);

module.exports = router;
▶ 试一试

10. 入口文件 app.js

▶ 示例:app.js

JAVASCRIPT
const express = require("express");
const booksRoute = require("./routes/books");
const { errorHandler } = require("./middleware/errorHandler");

const app = express();
app.use(express.json());

app.use("/api/books", booksRoute);
app.use(errorHandler);

const PORT = 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
▶ 试一试

11. 项目架构总览

Alice 画了一张架构图,清晰展示请求流向:

100%
graph TD
    A[app.js 入口] --> B[routes/books.js]
    B --> C{需要验证?}
    C -->|POST/PUT| D[middleware/validate.js]
    C -->|GET/DELETE| E[controllers/bookController.js]
    D --> E
    E --> F[models/bookModel.js]
    F --> E
    E --> G[统一响应格式]
    G --> H[客户端]
    E -->|异常| I[middleware/errorHandler.js]
    I --> H


12. API 测试

Bob 使用 curl 逐个验证每个端点:

curl 命令 说明
curl localhost:3000/api/books 获取所有书籍
curl localhost:3000/api/books/1 获取指定书籍
curl -X POST -H "Content-Type: application/json" -d '{"title":"New Book","author":"Bob","year":2025}' localhost:3000/api/books 新增书籍
curl -X PUT -H "Content-Type: application/json" -d '{"year":2026}' localhost:3000/api/books/1 更新书籍
curl -X DELETE localhost:3000/api/books/1 删除书籍

▶ 示例:测试新增与查询

BASH
# 新增一本书
curl -X POST -H "Content-Type: application/json" \
  -d '{"title":"Express in Action","author":"Evan","year":2024}' \
  localhost:3000/api/books
TEXT 📖 仅展示
{"success":true,"data":{"id":2,"title":"Express in Action","author":"Evan","year":2024}}
BASH
# 查询所有书籍
curl localhost:3000/api/books
TEXT 📖 仅展示
{"success":true,"data":[{"id":1,"title":"Node.js Guide","author":"Alice","year":2024},{"id":2,"title":"Express in Action","author":"Evan","year":2024}]}


13. 综合示例:完整书籍管理 API

将前面所有模块组合,以下是完整项目的核心代码流程:

JAVASCRIPT
// models/bookModel.js
const books = [{ id: 1, title: "Node.js Guide", author: "Alice", year: 2024 }];
let nextId = 2;
function findAll() { return books; }
function findById(id) { return books.find(b => b.id === id); }
function create(data) {
  const book = { id: nextId++, ...data };
  books.push(book);
  return book;
}
function update(id, data) {
  const index = books.findIndex(b => b.id === id);
  if (index === -1) return null;
  books[index] = { ...books[index], ...data };
  return books[index];
}
function remove(id) {
  const index = books.findIndex(b => b.id === id);
  if (index === -1) return false;
  books.splice(index, 1);
  return true;
}
module.exports = { findAll, findById, create, update, remove };
JAVASCRIPT
// middleware/validate.js
function validateBook(req, res, next) {
  const { title, author } = req.body;
  const errors = [];
  if (!title || typeof title !== "string") errors.push("title is required");
  if (!author || typeof author !== "string") errors.push("author is required");
  if (errors.length > 0) {
    return res.status(400).json({
      success: false,
      error: { code: 400, message: errors.join("; ") }
    });
  }
  next();
}
module.exports = { validateBook };
JAVASCRIPT
// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
  const status = err.status || 500;
  res.status(status).json({
    success: false,
    error: { code: status, message: err.message || "Internal Server Error" }
  });
}
function createError(status, message) {
  const err = new Error(message);
  err.status = status;
  return err;
}
module.exports = { errorHandler, createError };
JAVASCRIPT
// controllers/bookController.js
const Book = require("../models/bookModel");
const { createError } = require("../middleware/errorHandler");
function getAllBooks(req, res) {
  res.json({ success: true, data: Book.findAll() });
}
function getBookById(req, res, next) {
  const book = Book.findById(Number(req.params.id));
  if (!book) return next(createError(404, "Book not found"));
  res.json({ success: true, data: book });
}
function createBook(req, res) {
  const book = Book.create(req.body);
  res.status(201).json({ success: true, data: book });
}
function updateBook(req, res, next) {
  const book = Book.update(Number(req.params.id), req.body);
  if (!book) return next(createError(404, "Book not found"));
  res.json({ success: true, data: book });
}
function deleteBook(req, res, next) {
  const removed = Book.remove(Number(req.params.id));
  if (!removed) return next(createError(404, "Book not found"));
  res.json({ success: true, data: { message: "Book deleted" } });
}
module.exports = { getAllBooks, getBookById, createBook, updateBook, deleteBook };
JAVASCRIPT
// routes/books.js
const express = require("express");
const router = express.Router();
const ctrl = require("../controllers/bookController");
const { validateBook } = require("../middleware/validate");
router.get("/", ctrl.getAllBooks);
router.get("/:id", ctrl.getBookById);
router.post("/", validateBook, ctrl.createBook);
router.put("/:id", validateBook, ctrl.updateBook);
router.delete("/:id", ctrl.deleteBook);
module.exports = router;
JAVASCRIPT
// app.js
const express = require("express");
const booksRoute = require("./routes/books");
const { errorHandler } = require("./middleware/errorHandler");
const app = express();
app.use(express.json());
app.use("/api/books", booksRoute);
app.use(errorHandler);
app.listen(3000, () => console.log("Server running on port 3000"));
BASH
# 启动并测试
node app.js
curl -X POST -H "Content-Type: application/json" \
  -d '{"title":"Clean Code","author":"Robert","year":2008}' \
  localhost:3000/api/books
curl localhost:3000/api/books/1
curl -X DELETE localhost:3000/api/books/1

❓ 常见问题

Q controller 和 route 为什么要分开?
A 关注点分离——路由只定义路径映射,controller 专注业务逻辑,两者解耦后可独立修改和测试。
Q 如何测试 API?
A 命令行用 curl,图形界面用 Postman 或 VS Code 的 Thunder Client 插件,自动化测试用 supertest 库。
Q 数据验证应该在哪一层?
A 放在中间件层,在请求到达 controller 之前就拦截无效数据,保证 controller 逻辑干净。
Q 如何处理未找到的资源?
A 返回 404 状态码加统一错误格式 { success: false, error: { code: 404, message: "Book not found" } }
Q 项目应该用 TypeScript 吗?
A 小型练习项目用 JavaScript 足够;大型生产项目推荐 TypeScript,能获得类型安全和更好的 IDE 支持。
Q 内存数据存储重启后会丢失吗?
A 是的,数组存储在进程内存中,服务重启数据清空。生产环境应使用数据库(MongoDB / PostgreSQL 等)。
Q PUT 和 PATCH 有什么区别?
A PUT 要求提供完整资源做全量替换,PATCH 只传需要修改的字段做局部更新。本课用 PUT 简化实现。

📖 小节


📝 作业

  1. 完成本课所有代码示例,确保每个示例都能正确运行
  2. 修改综合示例,添加自己的扩展功能
  3. 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
  4. 思考:在实际项目中,你会如何应用本课学到的知识?
  5. 尝试将本课知识与前面课程的内容结合,构建一个小项目
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

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

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