首先完成“第 2 课:搭建 Node.js 和 Express 项目”即可解锁本课程。转到上一课

简介

当你的 API 端点数量超过寥寥几个时,把所有东西都塞进单个文件很快就会变得难以管理。Express 提供了一个内置工具 Router,让你按资源将路由拆分到不同的模块中。配合控制器层,你就拥有了一个能够干净扩展的结构——新成员可以快速找到需要的东西,新增资源也不会触动已有代码。

本课将带你走过一个具体的项目布局:路由负责定义 URL 模式和 HTTP 方法,控制器承载实际的处理逻辑,app.js 负责将它们串联起来。这种分层习惯是越早建立越重要的一项基本功。

分层架构示意图:请求流入 app.js,然后进入 Router 层,再到 Controller 层,最后到达一个作为占位的 Data 层

项目结构

下面是我们将要搭建的目录布局:

text
project/
├── app.js
├── routes/
│ ├── users.js
│ └── products.js
└── controllers/
├── usersController.js
└── productsController.js

模式很简单:每个资源对应一个路由文件,每个资源对应一个控制器文件。路由文件保持精简——只声明每个端点由哪个控制器函数处理;控制器保持专注——只包含针对该资源的请求/响应逻辑。

Express Router

Express Router 会创建一个迷你应用来独立处理路由,然后你在 app.js 中以某个路径将其挂载。

首先编写 users 路由文件:

javascript
// routes/users.js
const express = require('express');
const router = express.Router();
const usersController = require('../controllers/usersController');
router.get('/', usersController.getAllUsers);
router.get('/:id', usersController.getUserById);
router.post('/', usersController.createUser);
router.put('/:id', usersController.updateUser);
router.delete('/:id', usersController.deleteUser);
module.exports = router;

注意路由中使用的是相对路径,例如 / 和 /:id,而不是 /users 或 /users/:id。前缀会在 app.js 中挂载 router 时统一加上,这样你就无需重复书写。

在 app.js 中挂载该 router:

javascript
// app.js
const express = require('express');
const app = express();
app.use(express.json());
const usersRouter = require('./routes/users');
const productsRouter = require('./routes/products');
app.use('/users', usersRouter);
app.use('/products', productsRouter);
app.listen(3000, () => console.log('Server running on port 3000'));

现在,对 /users/42 发出的 GET 请求会匹配到 router.get('/:id', ...),且 req.params.id 等于 '42'。清晰且可预测。

控制器

控制器就是普通的模块,以具名函数的形式导出。每个函数都是一个标准的 Express 请求处理函数——接收 req 和 res(以及可选的 next),并负责发送响应。

javascript
// controllers/usersController.js
// Simulated in-memory data — you will replace this with a database call later
const users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
exports.getAllUsers = (req, res) => {
res.json(users);
};
exports.getUserById = (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
res.json(user);
};
exports.createUser = (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ message: 'name and email are required' });
}
const newUser = { id: users.length + 1, name, email };
users.push(newUser);
res.status(201).json(newUser);
};
exports.updateUser = (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
const { name, email } = req.body;
if (name) user.name = name;
if (email) user.email = email;
res.json(user);
};
exports.deleteUser = (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ message: 'User not found' });
}
users.splice(index, 1);
res.status(204).send();
};

这里有几点值得注意。控制器会校验输入,发现问题时立即以合适的状态码提前返回——这让正常路径的代码更易阅读。404 检查是显式进行的:不要假设记录一定存在。而成功的删除操作,其约定俗成的响应是 204 No Content 且不返回响应体。

流程图:单个传入的 HTTP 请求被 Router 匹配到正确的 Controller 函数,然后分流为成功响应或错误响应

常见错误

忘记解析 JSON 请求体是初学者最常遇到的问题。如果 app.js 中没有 app.use(express.json()),req.body 将是 undefined,createUser 或 updateUser 会在悄无声息中收到空数据。

另一个常见坑是把 req.params.id(字符串类型)直接用严格相等与数字类型的 id 比较。users.find(u => u.id === req.params.id) 这样的检查永远会失败,因为 '42' !== 42。请先用 parseInt 或 Number() 进行转换。

最后,发送响应后没有 return 会导致 "Cannot set headers after they are sent"(无法在已发送响应头之后再次设置)错误。务必使用 return res.status(404).json(...)——return 能阻止 Express 继续执行后续代码、避免重复发送响应。

小结

Express Router 让你将相关的路由归入各自独立的模块,并在 app.js 中以某个前缀挂载它们。控制器把处理逻辑从路由文件中剥离出来,使每一层都更易阅读、测试和扩展。整体模式是:路由文件声明 URL 与方法,控制器函数处理逻辑并发送响应。坚持每个资源对应一个文件,随着项目增长,这样的结构依然稳固可靠。

课后练习

课程检查点

巩固所学知识,获取作业反馈。

0/1
已回答 · 0/1
第3课:路由、控制器与项目结构