Node.js: RESTful API 项目
最后更新:2026-08-26
1. 项目背景
Alice 和 Bob 接到一个任务:为一家在线书店开发书籍管理 API。Alice 负责设计路由和中间件架构,Bob 负责编写业务逻辑。两人约定统一响应格式和错误处理规范,2 小时内完成了从项目搭建到完整 CRUD 的全部功能。
(1) 你将学到
- 综合运用 Express + 路由 + 中间件构建完整 API
- 项目目录结构设计(routes / controllers / middleware / models)
- 内存数据存储与完整 CRUD 操作实现
- 请求验证与统一错误处理
- API 测试方法(curl / Postman)
- 统一响应格式设计
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 画了一张架构图,清晰展示请求流向:
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 简化实现。
📖 小节
- 项目背景的核心概念与使用方法
- 项目目录结构设计的核心概念与使用方法
- API 端点设计的核心概念与使用方法
- 统一响应格式的核心概念与使用方法
- 数据模型层的核心概念与使用方法
- 请求验证中间件的核心概念与使用方法
- 错误处理中间件的核心概念与使用方法
- 控制器层的核心概念与使用方法
📝 作业
- 完成本课所有代码示例,确保每个示例都能正确运行
- 修改综合示例,添加自己的扩展功能
- 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
- 思考:在实际项目中,你会如何应用本课学到的知识?
- 尝试将本课知识与前面课程的内容结合,构建一个小项目