做后端开发这些年,我见过太多项目是从复制粘贴另一个项目开始的。尤其是Node.js后端项目模版这件事,看起来只是搭一个空壳,实际上一不小心就把上个项目里的历史包袱全带过来。我在团队里维护了一套后端项目模版,前后折腾了几轮,从最初的Express全家桶到现在这套结构清晰、开箱即用的骨架,新服务启动时间从半天缩短到十几分钟。这篇文章就把我的完整思路、目录设计、关键代码和踩坑记录梳理出来,给那些准备用Node.js起新项目,或者正在纠结模板怎么搭的朋友做个参考。不管你是刚转后端、想做个前后端分离的小项目,还是团队里要批量创建微服务,这套模板的思路都可以直接拿去用。
1. 为什么每次新项目都要从零搭一遍
1.1 重复劳动背后藏着的真实成本
我见过一个很典型的现象:很多团队起新后端服务的时候,流程是找一个老项目,把目录复制一遍,然后把业务代码删掉再改配置。听起来很省事,但实际上每一次复制都会把老项目里的历史包袱带过来,比如某个还没升级到新接口的依赖、某个只在上一个项目里成立的硬编码路径、甚至是一份早就没人看得懂的加密逻辑。
单看一次复制的成本不高,但如果你维护过五个以上服务,会发现每个服务渐渐长得都不一样了。A服务用morgan打日志,B服务用winston,C服务干脆什么日志都不打。错误处理也是五花八门,有的用next()把错误丢给Express默认处理,有的自己封装了一个非标准格式的失败对象。等到线上出问题要排障的时候,排查成本高得离谱。
这里有一个非常直观的对比,我在团队内部讲过很多次:
| 环节 | 没有模板 | 有模板 |
|---|---|---|
| 新建项目到跑通 | 约半天到一天 | 五到十分钟 |
| 日志和错误处理规范 | 每个项目各不相同 | 完全统一 |
| 新人理解项目结构 | 靠人传人或考古旧代码 | 看目录就知道 |
| 依赖升级 | 每个服务单独处理 | 模板统一升级后同步落新项目 |
| 线上排障 | 不同格式来回切换 | 一套格式通吃 |
所以模板不是写给懒人的偷懒工具,它是把"踩过的坑+约定好的规范+基础设施代码"沉淀成一份可执行的起点。
1.2 模板帮你省下的是沟通和试错成本
很多人觉得模板的价值是节省初始化时间,这个说法太浅了。模板真正的作用,是把你和团队在无数个项目里用教训换来的约定,固化到文件里。
举几个例子。目录该怎么组织?错误码怎么返回?配置项放哪?日志里必须带哪些字段?环境变量怎么区分开发和生产?这些如果只靠口头约定,一定有人漏。但如果模板里就是那么写的,新人进来照着目录放代码,出的活至少框架上是统一的。
我这套模板里,连日志请求ID都预设好了。每次请求进来,自动生成一个requestId,日志里打上traceId,错误响应里也带上。所有服务只要基于模板创建,排障的时候用同一个requestId就能把网关、服务、数据库日志串起来。这个东西如果等出了问题再想,就太晚了。
1.3 这套模板适合哪些使用场景
适合的场景主要有三类:
- 个人开发者,想要一个可以直接跑的后端骨架,重点是快速验证想法,目录和配置比较规范,省得后面返工。
- 小团队,同时维护多个后端服务,统一技术栈和代码风格以后,人挪到哪个项目都不用重新适应。
- 团队内部做微服务拆分,每个服务不再需要重复搭建基础设施,拉模板、改配置、写业务即可。
不太适合的场景是:项目已经非常庞大并且有自己独特的架构约束,这时候模板只能提供参考;还有一些极小的脚本型工具,只有几十行代码,用模板反而笨重。要分清什么场景用模板、什么场景不用,这是经验。
2. 技术选型:先把地基打好再动手
2.1 Node.js版本与包管理器怎么定
Node.js版本这点必须放在第一位。我踩过大坑,之前有同事图新鲜直接装最新版,结果项目里某个依赖锁定的版本还不兼容,运行的时候崩得一塌糊涂。模板里我建议采用当前LTS版本,现在至少是Node.js 20以上。LTS的意思是长期维护版本,稳定性和依赖兼容性都有保证。不要追current。
包管理器,我建议团队统一一种。npm、pnpm、yarn都能用,但混着用会出问题。npm是Node自带的,学习成本最低,只是安装速度和人磁盘占用一般。pnpm胜在安装快、省磁盘,而且有内容寻址存储机制,多个项目共用一份依赖副本。如果你是个人用,推荐pnpm;如果团队里的人对命令行工具不敏感,用npm最稳。
模板仓库里只需要提交一种锁文件。我是用的pnpm,所以提交pnpm-lock.yaml;如果用npm,就提交package-lock.json。这两种锁文件不要混着提交,否则同一个项目不同人用不同命令装出来的依赖版本可能不一致。
2.2 Web框架:Express、Fastify还是NestJS
后端框架的选择会直接影响目录设计和代码组织方式。我先给一个对比,大家按需求选:
| 框架 | 生态成熟度 | 性能 | 学习成本 | 适合场景 |
|---|---|---|---|---|
| Express | 极高 | 中 | 低 | 通用后端、快速交付 |
| Fastify | 较高 | 高 | 中 | 高并发、对性能敏感的服务 |
| NestJS | 高 | 中 | 较高 | 大团队、强规范、TypeScript重度用户 |
模板本身我用Express来演示,因为它的中间件生态最完整,几乎任何问题都能找到现成人用的方案,而且国内大量前后端分离项目就是Express或Koa风格,理解起来没有门槛。如果你的团队更追求性能,完全可以把这个目录结构平移到Fastify上,路由和中间件的写法有一些差异,但分层思路是一样的。
NestJS则是一个更重的框架,自带依赖注入、模块系统、守卫和拦截器,适合大型项目和强规范团队。但如果你想保持轻量,NestJS这些概念反而是负担。
2.3 目录结构:按业务逻辑分层而不是按文件类型堆
我见过很多项目的目录是按文件类型堆的,比如所有controller放一个文件夹,所有service放一个文件夹。这种做法在项目小的时候还行,一旦业务多起来,要找一个订单相关的代码得在controller、service、model三个目录之间来回跳。
我的模板里采用这种偏分层的目录结构:
my-backend-template/ ├── src/ │ ├── config/ # 配置读取与校验 │ ├── controllers/ # 请求参数解析、调用service、返回响应 │ ├── middlewares/ # 中间件:鉴权、日志、错误处理等 │ ├── models/ # 数据模型定义与数据库访问 │ ├── routes/ # 路由注册与模块挂载 │ ├── services/ # 核心业务逻辑 │ ├── utils/ # 通用工具函数 │ ├── app.js # 搭建应用、加载中间件和路由 │ └── server.js # 启动HTTP服务 ├── tests/ ├── .env.example ├── .eslintrc.cjs ├── .prettierrc ├── .editorconfig ├── Dockerfile ├── docker-compose.yml └── package.jsonroutes和controllers分开,是为了让人一眼看清接口路径和业务逻辑。controllers只做参数校验、调用service、把结果转换成HTTP响应,不写业务。services负责真正的业务规则。models层处理数据库访问。如果你想做更彻底的模块化,也可以按业务模块分,比如src/modules/user/controller.js这类的结构,但对小项目来说,上述目录已经足够清晰。
2.4 配置管理:环境变量和配置文件别混在一起
配置管理最容易出问题的地方,就是开发、测试、生产环境分不清楚。我的原则是:环境变量只放"环境相关"的配置,比如端口、数据库地址、jwt密钥;不要放业务规则,比如某个阈值、某个开关,这类配置应该放到应用配置文件里,集中管理。
模板里使用dotenv来加载.env文件,但.env文件只提交示例,不提交真是直内容。仓库里放一个.env.example,把所有需要的变量名写清楚,并附上默认值注释:
NODE_ENV=development PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/template CORS_ORIGIN=http://localhost:3001然后在src/config/index.js里做统一读取和校验:
const dotenv = require('dotenv'); const path = require('path'); dotenv.config({ path: path.resolve(process.cwd(), '.env') }); const config = { env: process.env.NODE_ENV || 'development', port: parseInt(process.env.PORT, 10) || 3000, databaseUrl: process.env.DATABASE_URL, corsOrigin: process.env.CORS_ORIGIN || '*', }; if (!config.databaseUrl) { throw new Error('缺少环境变量 DATABASE_URL,请检查 .env 文件'); } module.exports = config;这里有个小技巧:启动时做环境变量必填校验,宁可启动失败也不要带着缺少关键配置的半成品跑起来。现代应用应该fail fast,配置缺了就让它在入口直接报错,而不是运行到一半的时候再炸。
3. 从零搭建到跑通:核心步骤全记录
3.1 初始化项目与依赖清单
先创建项目目录并初始化npm和git,这些都是基础操作,但每一步都有目的:
mkdir my-backend-template cd my-backend-template npm init -y git init接下来安装依赖。我把它分成两类说明一下:
运行时依赖:
npm install express dotenv cors helmet morgan compression- express:Web框架
- dotenv:加载.env文件
- cors:开启跨域
- helmet:设置安全HTTP头
- morgan:HTTP请求日志
- compression:gzip压缩响应体
开发依赖:
npm install --save-dev nodemon eslint prettier- nodemon:开发时监听文件变动自动重启
- eslint:代码规范检查
- prettier:代码格式统一
如果你要接入数据库,后面再装prisma。先不要一次装太多东西,模板要克制,运行时依赖越少,后续维护压力越小。
3.2 入口文件与服务启动
我强烈建议把app.js和server.js分开。app.js负责创建express应用,server.js负责真正监听端口。这样写测试的时候可以直接用supertest对app发出请求,不需要真的启动一个端口,避免端口冲突,也快很多。
src/app.js的核心代码:
const express = require('express'); const helmet = require('helmet'); const cors = require('cors'); const morgan = require('morgan'); const compression = require('compression'); const config = require('./config'); const routes = require('./routes'); const errorHandler = require('./middlewares/error-handler'); const notFoundHandler = require('./middlewares/not-found-handler'); const app = express(); app.use(helmet()); app.use(cors({ origin: config.corsOrigin })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(compression()); app.use(morgan(config.env === 'development' ? 'dev' : 'combined')); app.use('/api', routes); app.use(notFoundHandler); app.use(errorHandler); module.exports = app;中间件的顺序很重要。helmet和cors这类安全相关的中间件要放在最前面,这样任何请求进来先经过安全检查。express.json()必须在路由之前,否则拿不到请求体。404处理的中间件放在所有路由之后,这样路由都不匹配时才会走到它。错误处理中间件必须放在最后,而且它要有四个参数(req, res, next, err),Express才能识别它是错误处理中间件。
src/server.js:
const app = require('./app'); const config = require('./config'); const server = app.listen(config.port, () => { console.log(`Server is running at http://localhost:${config.port} in ${config.env} mode`); }); process.on('SIGTERM', () => { server.close(() => { process.exit(0); }); });监听SIGTERM是Docker环境下的优雅停机,一定要保留。Kubernetes或者Docker stop会发SIGTERM给进程,如果你不做处理,进程会立刻被强制杀掉,当前正在处理的请求会被打断,产生大量异常日志和客户端超时。
3.3 路由模块化与中间件装配
路由不要全部堆在index.js里,按业务模块拆开放。示例里的健康检查路由就可以作为一个标准模板:
src/routes/index.js:
const express = require('express'); const healthRoutes = require('./health.routes'); const userRoutes = require('./user.routes'); const router = express.Router(); router.use('/health', healthRoutes); router.use('/users', userRoutes); module.exports = router;src/routes/health.routes.js:
const express = require('express'); const healthController = require('../controllers/health.controller'); const router = express.Router(); router.get('/', healthController.check); module.exports = router;src/controllers/health.controller.js:
exports.check = async (req, res) => { res.status(200).json({ status: 'ok', timestamp: new Date().toISOString(), }); };这种写法虽然多了一层文件,但每一层职责非常清楚。路由只做URL映射,controller做参数整理和响应输出。以后加一个用户模块,只需要新增user.routes.js、user.controller.js和user.service.js,然后到routes/index.js里挂一下即可,不需要动其他文件。
3.4 统一异常处理与日志体系
统一异常处理是模板里面价值最高的部分之一。没有它,代码里到处都是try/catch,或者错误直接抛到Express默认错误页,接口返回的是HTML格式的异常信息,前端根本没法处理。
我先写一个工具函数,用来包装异步路由:
src/utils/async-handler.js:
module.exports = function asyncHandler(fn) { return function (req, res, next) { Promise.resolve(fn(req, res, next)).catch(next); }; };在路由里就可以这么用,所有async函数抛出的异常都会自动传到errorHandler:
const asyncHandler = require('../utils/async-handler'); router.get( '/', asyncHandler(async (req, res) => { const userList = await userService.getUsers(); res.json(userList); }) );统一错误处理中间件:
src/middlewares/error-handler.js:
const logger = require('../utils/logger'); module.exports = function errorHandler(err, req, res, next) { const status = err.status || err.statusCode || 500; const message = err.expose ? err.message : '服务器内部错误'; if (status >= 500) { logger.error(`[${req.id}] ${err.stack || err.message}`); } res.status(status).json({ code: status, message, requestId: req.id, }); };这里的关键点:对外暴露的错误信息不能带服务器内部堆栈,否则很容易泄露敏感信息。日志里记录完整堆栈,但响应用户的是通用提示。404处理则单独一个中间件:
module.exports = function notFoundHandler(req, res) { res.status(404).json({ code: 404, message: `找不到 ${req.method} ${req.originalUrl}`, }); };日志体系方面,morgan负责HTTP访问日志,winston负责业务日志和错误日志,两者分开使用。morgan是请求层面的,winston是代码层面主动打的。模板里我封装了一个轻量logger:
const winston = require('winston'); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [new winston.transports.Console()], }); module.exports = logger;3.5 数据库接入与健康检查
数据库这块我用Prisma做示例。它是现在Node.js生态里最顺手的ORM之一,类型安全、迁移工具完善、和TypeScript配合非常好。即使是JavaScript项目,也能享受到schema文件带来的清晰感。
安装:
npm install @prisma/client npm install --save-dev prisma npx prisma init --datasource-provider postgresqlprisma/schema.prisma里至少定义一个示例模型:
generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(uuid()) email String @unique name String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }生成client:
npx prisma generatesrc/models/prisma.js:
const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); module.exports = prisma;健康检查路由里可以加数据库连通性检测:
const prisma = require('../models/prisma'); exports.check = async (req, res) => { let dbStatus = 'up'; try { await prisma.$queryRaw`SELECT 1`; } catch (err) { dbStatus = 'down'; } res.status(200).json({ status: 'ok', db: dbStatus, timestamp: new Date().toISOString(), }); };这个健康检查接口是给部署用的,不只是给人看。Docker/Kubernetes的探针会周期性地请求/health,如果应用进程还在但数据库已经挂了,这个接口能准确把状态暴露出来。
4. 工程化配置:让模板直接达到生产可用
4.1 ESLint + Prettier:把代码风格锁死
代码风格这种事,靠Code Review讨论太累了。直接在模板里配好ESLint和Prettier,创建新项目之后跑一次format,全项目风格就统一了。
我用的ESLint是8.x,原因是可以继续使用.eslintrc.cjs这种传统配置格式,很多现成配置和教程可以直接复用。ESLint 9改成了扁平配置,初期迁移成本还挺高的,对模板来说没有必要追这个新。
安装:
npm install --save-dev eslint@^8.57.0 prettier eslint-config-prettier eslint-plugin-prettier创建.eslintrc.cjs:
module.exports = { root: true, env: { node: true, es2022: true, }, extends: ['eslint:recommended', 'plugin:prettier/recommended'], parserOptions: { ecmaVersion: 2022, sourceType: 'script', }, rules: { 'no-console': 'warn', 'prettier/prettier': 'error', }, };.prettierrc:
{ "semi": true, "singleQuote": true, "trailingComma": "all", "printWidth": 100, "tabWidth": 2 }然后在package.json里加入:
"scripts": { "lint": "eslint . --ext .js", "format": "prettier --write \"**/*.{js,json,md}\"" }4.2 Git提交检查与团队协作基础
模板里可以把husky和lint-staged加上,让每次提交代码前自动跑lint,防止不合格代码进仓库。这个属于团队成熟后必备的工具,个人项目可以跳过。
安装:
npm install --save-dev husky lint-staged npx husky installpackage.json里配置lint-staged:
"lint-staged": { "*.js": "eslint --fix", "*.{js,json,md}": "prettier --write" }.husky/pre-commit文件:
#!/usr/bin/env sh npx lint-staged它的作用是:本地提交代码的瞬间,自动lint并把格式修正,如果lint失败,提交会直接拦截。这样代码进到远端分支前,已经把大部分低级问题挡在门外。
4.3 自动化测试骨架
模板里至少预留一套最小可用的测试骨架。我推荐vitest,比jest更现代、配置更少、跑得更快。安装:
npm install --save-dev vitest supertesttests/health.test.js:
const request = require('supertest'); const app = require('../src/app'); describe('GET /api/health', () => { it('应返回 ok 状态', async () => { const res = await request(app).get('/api/health').expect(200); const body = res.body; if (body.status !== 'ok') { throw new Error(`期望 status=ok,实际是 ${body.status}`); } }); });package.json里加:
"scripts": { "test": "vitest run", "test:watch": "vitest" }注意这里直接用app实例而不启动server,这是当初把app和server拆开的直接收益。测试过程中完全不占端口,跑完就结束,不会出现测试之间端口冲突。
4.4 Docker容器化与部署准备
模板里一定要预置Dockerfile和docker-compose.yml,否则新建服务后第一步到处查怎么部署,又要浪费不少时间。
Dockerfile:
FROM node:20-alpine WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN npm install --frozen-lockfile COPY . . EXPOSE 3000 CMD ["npm", "start"]docker-compose.yml:
services: api: build: . ports: - "3000:3000" environment: NODE_ENV: production PORT: 3000 DATABASE_URL: postgresql://postgres:postgres@db:5432/template CORS_ORIGIN: http://localhost:3001 depends_on: db: condition: service_healthy db: image: postgres:16-alpine environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: template ports: - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5使用npm install --frozen-lockfile而不是npm install,是为了依赖完全按照锁文件安装,避免本地和CI环境版本不一致。这是很多线上事故的根源,必须养成习惯。
5. 高频问题排查与避坑实录
5.1 Node.js版本切换和安装失败的坑
很多新人习惯从官网下载最新版,但等来的不是稳定而是兼容性问题。我记得有个热词是"error installing 24.21.0: node.js v24.21.0 is not yet released or is not ava",我看到那台机器上node版本管理工具试图安装一个还不存在的预发布版本号。
正确做法是使用nvm。安装nvm之后,维护三套长期使用的版本就够了,不要每个项目换一个版本。我的个人实践是模板锁定一个Node版本范围,在package.json里用engines字段声明:
"engines": { "node": ">=20 <21" }然后项目根目录放一个.nvmrc文件,内容只写版本号:
20.18.0这样只要团队用nvm,进入项目目录后一条命令nvm use,就能自动切到项目需要的版本。省去互相喊"你那边为什么跑不起来?哦,因为你的node版本不对"这种无意义沟通。
5.2 npm安装依赖失败:锁文件与镜像源
npm install失败的原因很多,最直接的排查路线是先看错误提示最后几行。常见的几种:
- EACCES权限问题:不要用sudo装全局包,应该修复npm目录权限或者用nvm管理Node。
- 版本冲突:node_modules里已经装过某个包的旧版本,依赖解析失败。把node_modules整个删掉,重新npm install。
- 网络下载超时:可以临时切换镜像源。国内常用的是npmmirror的registry:
npm config set registry https://registry.npmmirror.com使用pnpm的话对应为:
pnpm config set registry https://registry.npmmirror.com注意不要随意升级大版本依赖。模板锁文件的作用就是让所有人在同一个依赖版本集下运行。如果必须升某个依赖,单独起一个分支升级并跑完测试,不要在主分支上顺手升一下。
5.3 端口占用和进程清理
开发时最常见的就是端口3000被占用,一启动立刻报EADDRINUSE。
Linux/macOS下:
lsof -i :3000 kill -9 <PID>Windows下:
netstat -ano | findstr :3000 taskkill /PID <PID> /F不过在高频出现的场景里,我更推荐让开发端口尽量固定3000,同时代码里做一个小小的容错。你可以启动前先检测端口,被占用就直接报出清晰的提示,提示用户哪个进程占用了端口,而不是给一个冷冰冰的底层错误。注意,开发环境固定端口没问题,生产环境的端口应该交给容器编配或平台配置,不要写死。
5.4 Windows / Linux 跨平台路径问题
Node.js后端项目在很多团队里是跨平台开发的,有人用Windows,有人用macOS,服务器是Linux。这些平台最大的差异之一就是路径分隔符,Windows用反斜杠\,Linux和macOS用/。
如果你的代码里出现:
const filePath = __dirname + '\\src\\temp';那项目到Linux上必挂。正确做法是用path模块:
const path = require('path'); const filePath = path.join(__dirname, 'src', 'temp');另外在package.json的scripts里,如果需要设置NODE_ENV,Windows上用NODE_ENV=production node server.js会失效,因为Windows cmd不支持这种语法。统一使用cross-env来跨平台设置环境变量:
npm install --save-dev cross-env"scripts": { "start": "cross-env NODE_ENV=production node src/server.js" }这个坑我第一次遇到的时候排查了快一个小时,后来直接把cross-env写进模板的scripts里,再也不讨论哪条命令在Windows不兼容了。
5.5 异步接口异常导致进程崩溃
Express 4里,async函数中抛出的异常不会自动进入错误处理中间件。如果用户请求的接口内部有一步异步操作报错,不会变成500响应的JSON,而是会变成一个未处理的Promise rejection,严重情况下直接把Node进程搞挂,后续所有请求全部失败。
这就是我在3.4里写asyncHandler的价值。所有异步路由处理器都包一层,把异常统一转给next。如果你用Fastify,它对async/await的异常捕获天然支持得更好,这也是Fastify的一个优势。但Express的场景下,请一定保持asyncHandler的习惯。
排查这类问题还有一个技巧:在入口文件加一个全局的unhandledRejection监听,至少让进程在崩溃前把日志打出来:
process.on('unhandledRejection', (reason) => { logger.error(`未处理的Promise异常: ${reason}`); });这不是救命稻草,真正的解法还是每个异步路由都正确捕获错误,但监听器能帮助你在开发阶段尽早发现漏网之鱼。
6. 模板落地之后怎么维护和演进
6.1 模板仓库的版本管理与变更记录
模板不是一个一次性交付的交付物,它本身就是需要持续维护的代码仓库。我会建议单独建一个模板仓库,用git tag来标记版本。比如v1.0.0、v1.1.0,每次有比较大的调整,就在CHANGELOG.md里记录变更点。
使用模板的方式不要直接复制老项目,而是从模板仓库拉一个新的分支或者用git clone到一个新目录。有条件的话可以做一个小脚手架CLI,一字不漏地复制模板仓库,然后自动替换项目名、包名、端口等变量。没有CLI也没关系,clone下来然后全局搜索替换'backend-template'即可。
团队里其它服务用了某个早期版本的模板,后续模板升级了,不必追着老项目改,因为老项目可能已经长出自己独有的业务和架构。模板的价值在于新项目从最新的基线开始,老项目只需要在必要时做定向修复。
6.2 团队内统一模板的推广经验
我在团队里推广这套模板的过程中,遇到过不少抵触。最常见的反馈是"这个目录结构和我们之前不一样,看起来不习惯",或者"加这么多工具,学习成本太高了"。
后来我总结出一个特别有用的做法:不要着急一步到位。第一版模板先只做目录结构、入口文件、配置、日志和错误处理这些最核心的部分。跑通之后,再逐步加入Docker、测试、linter、husky。每加一项都要写清楚理由。等团队尝到甜头,再继续扩展。
还有一点比模板本身更重要的,就是模板的代码质量要足够高。它会被所有人当作代码范式来参照。如果模板本身里面有些历史遗留写法,大家就会一直照着写。定期评审模板代码,把它当作一等公民来维护,这是我一直坚持的事。
6.3 后续还能扩展什么
这套模板我已经用了相当长一段时间,目前大部分新服务都能在几分钟内拉好并本地跑通。如果再往深走,有几个方向值得做:
- 支持TypeScript:现在很多团队已经整体切TypeScript了,模板可以增加一个ts分支。目录结构不变,换成tsconfig、类型化的Controller和Service。
- 接入更多中间件:比如接口限流、接口幂等、JWT鉴权、权限校验,这些可以做成可选模块,按需勾选。
- 增加更多数据库适配:Prisma的Schema可以同时管理多套数据库,模板里预留一个migrations目录,方便不同的服务使用不同数据库。
- 增加CI/CD流水线:GitHub Actions或者GitLab CI模板,让新项目创建后自动跑lint、测试和镜像构建。
我个人做模板这件事最大的体会是,模板的价值不在于省那十几分钟,而在于把团队的共识写进文件里。每个人看到同样的目录结构、同样的错误处理格式、同样的日志规范,协作起来几乎不需要花时间解释"我们的项目是怎么组织的"。如果你也想搭一套自己的Node.js后端项目模版,建议从目录结构、入口拆分、统一错误处理和健康检查这四个最小核心开始,先把这四样做好,整个模板的骨架就立住了。后面需要什么,再往这个骨架上长就够了。