3. 构建第一个 Node.js 微服务
微服务的第一步不是“拆得多细”,而是先让一个服务能够独立启动、独立监听端口、独立暴露接口,并且能被外部稳定探测到存活状态。本节我们用一个“商品查询服务”作为示例,使用 Express 或 Fastify 创建 HTTP 服务,定义健康检查与业务路由,通过环境变量管理端口与运行模式,最后在本地运行并用调试工具验证行为。示例中的端口、路径和返回数据均为教学假设,不代表任何真实线上配置。
一、选择框架:Express 与 Fastify 的取舍
Express 生态成熟、中间件丰富、资料多,适合快速搭建和团队已有 Express 经验的情况;Fastify 性能更高、内置 JSON Schema 校验和序列化优化,适合对吞吐和请求校验有要求的服务。两者都能满足“第一个微服务”的需求。下面以 Fastify 为主示例,因为它对路由定义、请求校验和日志输出更结构化;同时给出 Express 的等价写法,便于对照。
初始化项目时,先创建目录并生成 package.json,再安装依赖。Fastify 示例安装 fastify 与 fastify-plugin(后者用于插件封装,可选);Express 示例安装 express。开发期建议安装 nodemon 或使用 Node.js 内置的 --watch,避免每次修改都手动重启。
mkdir product-service && cd product-service
npm init -y
npm install fastify
npm install --save-dev nodemon
二、用环境变量配置端口与运行模式
微服务不应把端口、主机、日志级别写死在代码里。约定使用 PORT、HOST、NODE_ENV、LOG_LEVEL 四个环境变量:PORT 默认 3000,HOST 默认 0.0.0.0(便于容器或局域网访问),NODE_ENV 默认 development,LOG_LEVEL 默认 info。本地开发可以用 .env 文件配合 dotenv 加载,但生产环境应通过部署平台注入,不要把密钥提交到仓库。
# .env(仅本地开发使用,不要提交到 Git)
PORT=3000
HOST=0.0.0.0
NODE_ENV=development
LOG_LEVEL=info
读取时统一做一次解析和默认值兜底,避免每个模块各自读 process.env 导致行为不一致。例如:
const config = {
port: Number(process.env.PORT) || 3000,
host: process.env.HOST || '0.0.0.0',
env: process.env.NODE_ENV || 'development',
logLevel: process.env.LOG_LEVEL || 'info'
};
三、定义健康检查与业务路由
健康检查通常分两类:存活检查(liveness)只回答“进程还在不在”,不依赖下游;就绪检查(readiness)回答“能不能接流量”,可能依赖数据库或缓存。第一个微服务先实现存活检查 GET /health,返回 200 与固定结构;再实现业务路由 GET /api/products/:id,根据路径参数返回商品数据,找不到时返回 404。
Fastify 写法如下,注意路由注册、参数校验和错误处理:
const fastify = require('fastify')({ logger: { level: config.logLevel } });
fastify.get('/health', async () => {
return { status: 'ok', service: 'product-service', uptime: process.uptime() };
});
fastify.get('/api/products/:id', {
schema: {
params: {
type: 'object',
required: ['id'],
properties: { id: { type: 'string', pattern: '^[0-9]+$' } }
}
}
}, async (request, reply) => {
const { id } = request.params;
const product = { id, name: '示例商品', price: 99.9, stock: 10 };
if (id === '999') {
return reply.code(404).send({ code: 'NOT_FOUND', message: '商品不存在' });
}
return product;
});
fastify.listen({ port: config.port, host: config.host })
.then(() => fastify.log.info(`服务已启动:http://${config.host}:${config.port}`))
.catch(err => { fastify.log.error(err); process.exit(1); });
Express 等价写法更直观,但需要自己处理 JSON 解析和错误中间件:
const express = require('express');
const app = express();
app.use(express.json());
app.get('/health', (req, res) => {
res.json({ status: 'ok', service: 'product-service', uptime: process.uptime() });
});
app.get('/api/products/:id', (req, res) => {
const { id } = req.params;
if (id === '999') {
return res.status(404).json({ code: 'NOT_FOUND', message: '商品不存在' });
}
res.json({ id, name: '示例商品', price: 99.9, stock: 10 });
});
app.listen(config.port, config.host, () => {
console.log(`服务已启动:http://${config.host}:${config.port}`);
});
四、本地运行、调试与结果判断
启动命令建议在 package.json 中固定为 npm run dev,使用 nodemon src/server.js 或 node --watch src/server.js。启动后应观察日志中是否出现监听地址与端口;若端口被占用,会抛出 EADDRINUSE,此时应更换 PORT 或结束占用进程。调试时可用 node --inspect 配合 Chrome DevTools,在路由处理函数中打断点,观察 request.params、request.query 与返回值。
验证分三步:第一,访问 GET /health,期望 HTTP 200 且 status 为 ok;第二,访问 GET /api/products/1,期望返回商品 JSON;第三,访问 GET /api/products/999,期望 HTTP 404 且 code 为 NOT_FOUND。若返回 500,优先查看服务端日志中的堆栈;若返回 404 但路径正确,检查路由前缀与大小写;若连接被拒绝,检查 HOST 是否为 0.0.0.0 以及防火墙设置。
curl -i http://localhost:3000/health
curl -i http://localhost:3000/api/products/1
curl -i http://localhost:3000/api/products/999
下面这张图把本节的核心结构串起来:环境变量进入配置对象,配置对象驱动服务启动;服务内部同时挂载健康检查与业务路由;外部请求按路径分流,最终得到 200、404 或 500 三类可判断结果。图中同时标出本地调试时最值得观察的日志与断点位置。
把上述结构落地后,你就得到了一个可独立运行、可被探测、可被调试的最小微服务。后续接入数据库、消息队列或服务注册发现时,健康检查与配置读取这两条主线仍然保持不变,只是就绪检查会加入对下游依赖的判断。