把你的 API 拆分为职责单一的各层,并用轻量的依赖注入把它们连接起来,让它变得可测试。
在 Kodokon 中打开本课一个把路由、业务逻辑和数据访问混在一起的文件,最终总是代价高昂:你没法在不启动服务器的情况下测试业务规则,也没法在不重写一切的前提下替换基础设施。拆分成三层能同时解决这两个问题。路由声明 URL 并进行委派。控制器把 HTTP 翻译成业务调用:读取请求、选择状态码、序列化响应。服务持有业务规则,且对 Express 一无所知。依赖规则很严格:每一层只认识它下面的那一层,反过来绝不允许。
import { Router } from 'express';
export function createUserRouter(controller) {
const router = Router();
router.get('/', controller.list);
router.post('/', controller.create);
return router;
}控制器是一个把服务作为参数接收进来的工厂。它不持有任何业务规则:它的职责仅限于从 req 中提取数据、调用服务,再把结果转换成 HTTP 响应。任何错误都交给 next,由中央错误中间件来处理,绝不在各个控制器里散落地写 res.status(500)。
export function createUserController(service) {
return {
async list(req, res, next) {
try {
const users = await service.listUsers();
res.json(users);
} catch (err) {
next(err);
}
},
async create(req, res, next) {
try {
const user = await service.createUser(req.body);
res.status(201).json(user);
} catch (err) {
next(err);
}
},
};
}服务集中承载各项决策:邮箱唯一性、创建规则。它抛出业务错误(最多附带一个 status 字段),同时对 Express 一无所知。它同样把自己的依赖,也就是数据仓储,作为参数接收进来。这就是轻量注入:在启动时调用一次的普通工厂函数,取代了笨重的注入容器,并让每一层在测试中都可以被替换。
export function createUserService(repository) {
return {
listUsers() {
return repository.findAll();
},
createUser(input) {
const found = repository.findByEmail(input.email);
if (found) {
const err = new Error('Email already used');
err.status = 409;
throw err;
}
return repository.insert(input);
},
};
}剩下的就是组装根:唯一一处把各个工厂拼装到一起的地方。createApp 接收基础设施依赖(这里是数据库,下一课会讲),并返回一个完整但尚未开始监听的 Express 应用。把构造和监听分开看似微不足道;但正是它让测试能把 API 挂载到一个临时端口上,而生产环境的服务器仍然只有三行:创建数据库、创建应用、开始监听。
import express from 'express';
import { createUserRepository } from './db/users.js';
import { createUserService } from './services/users.js';
import {
createUserController,
} from './controllers/users.js';
import { createUserRouter } from './routes/users.js';
export function createApp({ db }) {
const app = express();
app.use(express.json());
const repository = createUserRepository(db);
const service = createUserService(repository);
const controller = createUserController(service);
app.use('/api/users', createUserRouter(controller));
return app;
}createApp 自己不开始在网络上监听?