Node.js 中如何正确设计多级路由结构?

Mr-采涵 阅读 10

我正在用原生 Node.js 写一个后端服务,想把用户相关的接口放在 /api/users 下,但不知道怎么组织路由文件才清晰。现在所有逻辑都堆在主入口里,代码越来越乱。

试过自己写中间件匹配路径前缀,但参数解析和子路由处理总是出问题。比如访问 /api/users/123 时,后面的 ID 拿不到。有没有推荐的路由拆分方式?

const http = require('http');

const server = http.createServer((req, res) => {
  if (req.url.startsWith('/api/users')) {
    // 这里怎么优雅地处理 /api/users/:id 和 /api/users 这种不同路径?
    res.end('user route');
  }
});

server.listen(3000);
我来解答 赞 2 收藏
二维码
手机扫码查看
1 条解答
Air-娇娇
这个问题很常见,用原生 Node.js 写后端确实需要自己处理路由。让我给你一个清晰可用的方案。

首先说下你之前拿不到 ID 的原因:直接用 startsWith 判断只能知道请求来了,但没把 URL 解析成可用的部分。

我给你两种方案,从简单到稍复杂,看你需求。

方案一:简单版,手动解析路径

const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
// 解析 URL,获取路径部分
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
const query = parsedUrl.query;

// 判断是否以 /api/users 开头
if (pathname.startsWith('/api/users')) {
// 去掉前缀,获取后面的部分
const subPath = pathname.slice('/api/users'.length);

// 处理 /api/users 这种请求(获取用户列表)
if (subPath === '' || subPath === '/') {
if (req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: '返回用户列表' }));
} else {
res.writeHead(405);
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
}
return;
}

// 处理 /api/users/123 这种请求(获取单个用户)
// subPath 现在是 "/123",去掉开头的斜杠
const userId = subPath.slice(1);

// 简单验证一下 ID 是不是数字
if (/^d+$/.test(userId)) {
if (req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: '返回单个用户',
userId: userId
}));
} else {
res.writeHead(405);
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
}
return;
}
}

// 没匹配到任何路由,返回 404
res.writeHead(404);
res.end(JSON.stringify({ error: 'Not Found' }));
});

server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});


运行后试试这些请求:
curl http://localhost:3000/api/users
curl http://localhost:3000/api/users/123

这种方式能跑,但缺点是所有逻辑堆在一起,路径判断一多就乱了。

方案二:进阶版,模块化路由系统

我给你写一个轻量级的路由框架,支持参数匹配和文件拆分:

先创建项目结构:

your-project/
├── server.js # 主入口
├── router.js # 路由核心
└── routes/
├── users.js # 用户路由
└── posts.js # 帖子路由(示例)


// router.js
// 路由核心模块,负责路径匹配和参数解析

const url = require('url');

// 存储所有注册的路由
const routes = [];

// 解析路径模式,转换成正则表达式
// 比如 /api/users/:id 会变成 ^/api/users/([^/]+)$
// 同时提取参数名 id
function parsePattern(pattern) {
const paramNames = [];
// 把 :xxx 替换成正则捕获组 ([^/]+)
const regexStr = pattern.replace(/:([^/]+)/g, (_, key) => {
paramNames.push(key);
return '([^/]+)';
});

return {
regex: new RegExp(^${regexStr}$),
paramNames
};
}

// 注册路由的方法
function addRoute(method, path, handler) {
const { regex, paramNames } = parsePattern(path);
routes.push({
method: method.toUpperCase(),
path,
regex,
paramNames,
handler
});
}

// 处理请求的入口
function handleRoute(req, res) {
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
const method = req.method.toUpperCase();

// 遍历路由表,找匹配的路由
for (const route of routes) {
// 先检查方法,再检查路径
if (route.method !== method) continue;

const match = pathname.match(route.regex);

if (match) {
// 提取参数,match[0] 是完整匹配,match[1] 开始是捕获组
const params = {};
route.paramNames.forEach((name, index) => {
params[name] = match[index + 1];
});

// 把参数和查询字符串挂载到 req 上,后面处理函数可以直接用
req.params = params;
req.query = parsedUrl.query;

// 执行处理函数
return route.handler(req, res);
}
}

// 没找到匹配的路由
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not Found' }));
}

// 辅助方法:统一返回 JSON 响应
function json(res, data, statusCode = 200) {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}

module.exports = { addRoute, handleRoute, json };


// routes/users.js
// 用户路由模块

const { addRoute, json } = require('../router');

// GET /api/users - 获取用户列表
addRoute('GET', '/api/users', (req, res) => {
// 这里可以调用数据库获取数据
const users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
];
json(res, { users });
});

// GET /api/users/:id - 获取单个用户
addRoute('GET', '/api/users/:id', (req, res) => {
// req.params.id 就是 URL 中的 123
const userId = req.params.id;

json(res, {
id: userId,
name: '用户' + userId
});
});

// POST /api/users - 创建用户
addRoute('POST', '/api/users', (req, res) => {
// 接收请求体需要额外处理,这里先简化
json(res, { message: '创建用户成功' }, 201);
});

// DELETE /api/users/:id - 删除用户
addRoute('DELETE', '/api/users/:id', (req, res) => {
const userId = req.params.id;
json(res, { message: 删除用户 ${userId} 成功 });
});


// routes/posts.js
// 帖子路由示例,演示多模块共存

const { addRoute, json } = require('../router');

// GET /api/posts - 获取帖子列表
addRoute('GET', '/api/posts', (req, res) => {
json(res, { posts: [] });
});

// GET /api/posts/:postId/comments/:commentId - 嵌套路径示例
addRoute('GET', '/api/posts/:postId/comments/:commentId', (req, res) => {
// 可以同时拿到两个参数
const { postId, commentId } = req.params;
json(res, {
postId,
commentId,
content: '这是评论内容'
});
});


// server.js
// 主入口

const http = require('http');
const { handleRoute } = require('./router');

// 先加载所有路由模块
require('./routes/users');
require('./routes/posts');

const server = http.createServer((req, res) => {
handleRoute(req, res);
});

server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
console.log('试试这些 URL:');
console.log(' GET /api/users');
console.log(' GET /api/users/123');
console.log(' GET /api/posts/1/comments/5');
});


运行后测试:

node server.js

# 测试各个接口
curl http://localhost:3000/api/users
curl http://localhost:3000/api/users/123
curl http://localhost:3000/api/posts/1/comments/5
curl http://localhost:3000/api/users/abc


关键点解释一下:

路由参数解析的原理是把 /api/users/:id 转换成正则 ^/api/users/([^/]+)$,然后用正则匹配 URL,捕获组里的内容就是参数值。 :id 中的 id 会作为参数名存
点赞
2026-03-13 16:01