搭建多模块 Node 后端 目录到底怎么划分才不靠直觉
Node 后端目录结构划分没有唯一标准,但有稳定可靠的套路。本文基于职责分离原则,讲解 app、config、routes、controllers、services、models、middlewares 等层级的职责边界,教你如何把稳定基础设施与变动业务代码分开,让查找和重构都不靠直觉。

问题背景
刚开始写 Node 后端那会儿,我习惯把所有东西都塞进 app.js ,路由、数据库连接、业务逻辑全挤在一起。等接口一多,几千行代码的文件改起来痛不欲生 - 改一个字段要翻半天,同事接手更是一脸懵。很多人问:目录结构到底有没有标准答案?我的回答是:没有唯一标准,但有稳定可靠的套路。
原理说明
目录划分的原则其实是"按职责,不按个人喜好"。核心思路是把 稳定不变的基础设施 和 天天变动的业务代码 区分开,二者用清晰的边界隔开。常见的分层是:
app/server:应用的组装入口,只负责把中间件、路由挂上去config:配置中心,负责读环境变量、加载配置文件routes(或controllers):接收 HTTP 请求,只做参数校验和调用下层,不写业务services:业务逻辑的归属地,可被复用、可被测试models:数据模型的抽象,跟 ORM / 数据库打交道middlewares:可复用的中间件utils/helpers:纯函数工具,不依赖框架tests:测试目录
这样的好处是:你要找一段跟订单相关的逻辑,顺着业务进 services ,永远不迷路。
示例代码
一个典型的多模块目录骨架:
my-server/
├── app.js # 应用组装入口
├── index.js # 进程启动文件(读配置、监听端口)
├── config/
│ ├── index.js # 加载配置
│ └── env.js # 环境解析
├── routes/
│ ├── index.js # 路由汇总
│ └── order.js # 订单相关路由
├── controllers/
│ └── orderController.js
├── services/
│ └── orderService.js
├── middlewares/
│ ├── auth.js
│ └── errorHandler.js
├── models/
│ └── order.js
├── utils/
│ └── httpResponse.js
└── tests/
└── order.test.jsindex.js 里只需要做一件事 - 把 app 拉起来:
const app = require('./app');
const config = require('./config');
const port = config.port || 3000;
app.listen(port, () => {
console.log(`server listening on ${port}`);
});app.js 只做组装:
const express = require('express');
const routes = require('./routes');
const { auth, errorHandler } = require('./middlewares');
const app = express();
app.use(express.json());
app.use('/api', routes); // 把路由挂到统一前缀
app.use(errorHandler); // 最后兜住异常
module.exports = app;常见坑与解决方案
- 坑一:所有文件怼在一个
routes里,几十个路由文件全挤在一起。解法:按业务域拆routes/order.js、routes/user.js,一个文件一个子路由。 - 坑二:把业务逻辑写进路由回调里,导致无法单测、无法复用。解法:路由只做参数拿取和响应,业务一律下沉到
services。 - 坑三:工具函数散落各处,同一段逻辑复制了三遍。解法:抽到
utils/,并且保持纯函数、无副作用,方便复用。 - 坑四:启动逻辑和组装逻辑不分家,一次环境就翻车。解法:
index.js只管跑起来,app.js只管组装,测试时直接引入app.js不用真的监听端口。
小结
目录划分从来不是一次成型的事,随着项目长大会生长。但只要守住"职责分离 + 稳定与变动分开"这条主线,后面重构的成本就会低很多。下一篇我们聊聊接口规划,接口多了以后怎么避免越写越乱。
FAQ(常见问题)
Q:是不是所有项目都必须拆这么多层目录?
A:不一定。目录结构要匹配项目规模,小项目过度拆分反而增加导航成本。建议从 app、routes、services、models、utils 这几个核心层起步,随业务变复杂再逐步扩展,重点守住"职责分离"的主线。
Q:业务逻辑到底放 routes 还是 services?
A:放 services。routes 只负责接请求、校验参数、给响应,真正的业务逻辑放到 services,才能被路由、定时任务、消息队列等多处复用,也方便单独测试。
Q:稳定基础设施和业务代码为什么要分开存放?
A:基础设施(如 config、middlewares、utils)改动频率低,业务代码变化频繁。分开存放能让边界清晰,改业务时不会牵连基础设施,天然的风险面和不可控范围更小。






