Appearance
Express 与 API 开发
Express 是 Node.js 生态里历史最久、社区最大的 Web 框架。运维场景里的轻量 API、webhook 接收器、内部管理面板,用 Express 比裸写 http.createServer 省很多事,路由、中间件、请求解析、错误处理都有现成的。但它也是"旧框架"的代表——底层不基于 Promise,和 modern JS 有点不一致,这点后面会说。
最小服务
bash
npm init -y
npm install expressjson
{
"type": "module",
"scripts": { "start": "node server.js" },
"dependencies": { "express": "^4.21.0" }
}js
// server.js
import express from "express";
const app = express();
const port = Number(process.env.PORT || 3000);
app.get("/", (req, res) => {
res.send("ok\n");
});
app.get("/healthz", (req, res) => {
// 健康检查端点是运维标配——K8s、负载均衡、监控都靠它判断服务存活
res.json({ ok: true, ts: new Date().toISOString() });
});
app.listen(port, "0.0.0.0", () => {
console.log(`listening on port ${port}`);
});0.0.0.0 绑定所有网卡接口。绑定 127.0.0.1 的话容器外部、其他机器都访问不到——容器里 localhost 只绑容器内部网络。
Express 的架构:中间件链
Express 的核心是一个中间件队列。每个请求经过所有注册的中间件,按注册顺序执行。中间件可以做三件事:修改 req/res、直接响应、传递给下一个中间件。
js
// 中间件签名:(req, res, next)
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next(); // 不调 next,请求就在这里卡住
});Express 4.x 不是基于 Promise 的。路由处理里 return 无效——必须调 res.send/json/end 或 next()。路由里写了 async 函数后如果抛异常,Express 4.x 不会自动捕获,请求会一直挂起。解决办法是统一用 next(err) 传递,或者写一个包裹函数。
路由:方法和路径
js
// 精确路径
app.get("/api/servers", (req, res) => {
res.json([{ name: "web-01", ip: "10.0.0.11" }]);
});
// 路径参数
app.get("/api/servers/:name", (req, res) => {
res.json({ name: req.params.name, status: "running" });
// :name 匹配到的值在 req.params.name 里
});
// 查询参数——?service=nginx&lines=50
app.get("/api/logs", (req, res) => {
const service = req.query.service || "nginx";
const lines = Number(req.query.lines || 100);
res.json({ service, lines });
});REST 路径风格的基本约定(不是规范,但团队一致性有帮助):
text
GET /api/servers 列表
GET /api/servers/:id 单个详情
POST /api/servers 创建
PATCH /api/servers/:id 部分更新
DELETE /api/servers/:id 删除请求体解析
Express 4.x 到 4.16 之前需要单独装 body-parser,现在内置了:
js
// limit 限制请求体大小——防大包拖垮服务
app.use(express.json({ limit: "1mb" }));
app.post("/api/jobs", (req, res) => {
const { name, command } = req.body;
// 内部接口也做校验——调用方何时变成脚本/定时任务/其他系统,坏输入迟早会出现
if (!name || !command) {
return res.status(400).json({ error: "name and command are required" });
}
res.status(201).json({ id: Date.now(), name, command, status: "created" });
});中间件:日志和鉴权
请求日志中间件:
js
app.use((req, res, next) => {
const start = Date.now();
res.on("finish", () => {
// finish 事件在响应已经发给客户端之后触发
console.log(JSON.stringify({
method: req.method,
path: req.path,
status: res.statusCode,
cost_ms: Date.now() - start,
ip: req.ip
}));
});
next();
});简单 token 鉴权——运维内部工具的底线,不是安全方案:
js
function requireToken(req, res, next) {
const expected = process.env.API_TOKEN;
const actual = req.header("x-api-token");
if (!expected || actual !== expected) {
return res.status(401).json({ error: "unauthorized" });
}
next();
}
// 只在需要保护的路由上挂
app.post("/api/jobs", requireToken, (req, res) => {
res.json({ ok: true });
});内部工具能被别人从内网访问到,就不要假设"都是自己人"。"内网"不是权限模型——一条 webhook 不小心暴露了操作入口,只需要一个 curl。
异步错误处理
这是 Express 4.x 最需要小心的地方。普通函数同步抛错 Express 会自己 catch 并交给错误处理中间件;async 函数抛 Express 4.x 不会自动捕获——必须 try/catch 后调 next(err):
js
// 每个异步路由都要 try/catch + next(err)
app.get("/api/config", async (req, res, next) => {
try {
const config = await loadConfig("config.json");
res.json(config);
} catch (err) {
next(err); // 交给统一错误处理
}
});
// 也可以用包装函数省掉重复 try/catch
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get("/api/services", asyncHandler(async (req, res) => {
const results = await checkAll();
res.json(results);
}));Express 5.x 原生支持 Promise 返回值——async 函数抛错自动交给错误处理。但升到 Express 5 要检查依赖(特别是 EJS、一些中间件)是否兼容。
统一错误中间件——四个参数的就是错误处理:
js
app.use((err, req, res, next) => {
// 完整错误进日志,接口响应只给固定格式
console.error(err);
const status = err.status || 500;
res.status(status).json({
error: status === 500 ? "internal server error" : err.message
});
});模板页面
运维小面板不需要 React/Vue。EJS 把数据嵌进 HTML 模板——只读展示和少量表单够用:
bash
npm install ejsjs
app.set("view engine", "ejs");
// 模板默认放在项目根目录的 views/ 下
app.get("/dashboard", async (req, res) => {
const results = await checkAll();
res.render("dashboard", {
title: "Service Status",
services: results
});
});html
<!-- views/dashboard.ejs -->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title><%= title %></title>
</head>
<body>
<h1><%= title %></h1>
<table>
<% for (const s of services) { %>
<tr>
<td><%= s.name %></td>
<td><%= s.active ? "active" : "inactive" %></td>
</tr>
<% } %>
</table>
</body>
</html>EJS 里三种标签:<%= %> 输出(转义 HTML)、<%- %> 输出(不转义,别用在外来数据上)、<% %> 逻辑语句(循环、条件)。交互变多时把页面拆成前后端分离,模板塞业务状态越塞越乱。
优雅退出
HTTP 服务被 systemd 停、Docker stop、K8s 滚动更新时,通常会收到 SIGTERM。不处理的话,进程被直接杀掉,正在处理的请求中断、连接池没关、写入半截:
js
const server = app.listen(port, "0.0.0.0", () => {
console.log(`listening on port ${port}`);
});
process.on("SIGTERM", () => {
console.log("received SIGTERM, shutting down");
// server.close() 停止接收新连接,等现有连接处理完成
server.close(() => {
console.log("server closed");
process.exit(0);
});
// 兜底:10 秒后就算有连接没处理完也强制退出
setTimeout(() => {
console.error("force exit after timeout");
process.exit(1);
}, 10_000).unref();
// unref() 让这个定时器不阻止进程退出——如果 server.close 先完成了
});Express 的局限
Express 4.x 不基于 Promise、不原生支持 async 路由错误捕获、没有内置的请求验证、没有类型安全。这几个问题在运维小 API 里能接受,但如果要做更复杂的——后台管理系统带权限、并发高的 API 网关——要考虑更现代的选择:
- Fastify:基于 Promise、Schema 校验内置、性能接近原生、插件体系干净
- Hono:更轻量,支持 Web Standard (Request/Response),边缘计算场景多
- Koa:Express 团队的下一代框架,基于 async/await 中间件,社区生态小
运维的工具链评估有个现实因素:同事接手后能不能快速改。Express 的生态和资料最多,对于内部 API 来说,稳定性比现代化程度更重要。