2. TypeScript后端项目初始化
一个TypeScript后端项目能否长期稳定演进,往往在初始化阶段就已经决定了大半。初始化不是简单地执行几条命令,而是要在运行时、包管理器、编译配置和目录结构四个层面做出明确取舍。本节围绕这四个层面展开,给出可直接落地的选型依据与配置样例。
一、Node.js运行时选型:先确定版本策略
TypeScript最终要编译成JavaScript交给Node.js执行,因此运行时的版本直接决定了可用的语言特性和API边界。选型时关注三个维度:LTS状态、ES模块支持程度、与依赖生态的兼容性。
Node.js的发布节奏是:偶数版本进入LTS(长期支持),奇数版本为短期实验版本。生产项目应始终选择处于Active LTS或Maintenance LTS的偶数版本,例如Node 20或Node 22。奇数版本(如21、23)虽然能提前体验新特性,但不适合作为后端服务的运行基础。
第二个关键点是模块系统。Node.js同时支持CommonJS(CJS)和ES Module(ESM)。ESM是标准方向,支持顶层await、更好的静态分析能力,但部分老依赖仍只提供CJS入口。一个稳妥的策略是:新项目优先采用ESM,并在package.json中声明"type": "module";如果依赖链中CJS占比过高,则退回CJS,避免在互操作上消耗过多精力。
第三个维度是版本锁定。团队协作中必须通过.nvmrc或package.json的engines字段固定Node版本,避免“本地能跑、CI报错”的经典问题:
{
"engines": {
"node": ">=20.11.0 <21"
}
}
二、包管理器对比:npm、pnpm、yarn
包管理器决定了依赖安装速度、磁盘占用和依赖树的确定性。三者的核心差异如下:
| 维度 | npm | pnpm | yarn (Berry) |
|---|---|---|---|
| 依赖存储方式 | 扁平化 node_modules | 全局内容寻址存储 + 硬链接 | PnP 或 node_modules |
| 磁盘占用 | 高(重复安装) | 低(多项目共享) | 中 |
| 幽灵依赖 | 存在 | 严格隔离,基本消除 | PnP 模式下消除 |
| 安装速度 | 中等 | 快 | 快 |
| Monorepo 支持 | workspaces | workspaces(体验最佳) | workspaces |
对于后端项目,推荐优先考虑pnpm:它的严格依赖隔离能有效避免“幽灵依赖”——即代码中引用了未在package.json声明的包,却因为扁平化安装而恰好能运行。这类问题在本地不易暴露,一旦部署到干净环境就会直接崩溃。pnpm通过符号链接构建严格的依赖树,让这类问题在开发阶段就暴露出来。
如果团队已有成熟的npm工作流且项目规模不大,npm配合package-lock.json也完全可用。关键是锁定lockfile并提交到版本库,确保所有环境安装出完全一致的依赖树。
三、tsconfig.json核心配置
tsconfig.json是TypeScript项目的编译契约。后端项目与前端项目的配置重点不同:后端更关注输出目录、模块格式、类型严格性和路径别名。以下是一份面向Node.js ESM后端项目的配置样例:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
逐项说明关键字段:
- target: ES2022——输出语法版本。Node 20+完整支持ES2022,无需降级到ES5,保留原生class、可选链等特性。
- module / moduleResolution: NodeNext——让TypeScript按照Node.js的ESM解析规则处理导入。使用ESM时,相对导入必须带
.js后缀,这是Node ESM的硬性要求,NodeNext会强制检查这一点。 - strict: true——开启全部严格检查,包括
strictNullChecks、noImplicitAny等。后端代码处理大量外部输入,严格模式能在编译期拦截大量空值和类型错误。 - outDir / rootDir——明确源码与产物的边界,避免编译产物污染源码目录。
- paths——路径别名,把
@/映射到src/,减少../../../式的相对路径。注意:路径别名需要配合运行时解析方案(如tsc-alias或打包工具),否则编译后的JS无法解析别名。 - declaration + sourceMap——生成类型声明和源码映射,便于调试和作为库被引用。
module和moduleResolution改为CommonJS和Node,相对导入不需要.js后缀。两种模式不要混用。四、项目目录结构设计
目录结构应体现职责分层,让新增代码时有明确的归属位置。一个适合中小型后端服务的结构如下:
project-root/
├── src/
│ ├── config/ # 环境变量加载与校验
│ ├── controllers/ # 请求处理,参数解析与响应组装
│ ├── services/ # 业务逻辑,不依赖HTTP层
│ ├── repositories/ # 数据访问,封装数据库操作
│ ├── models/ # 领域模型与类型定义
│ ├── middlewares/ # 认证、日志、错误处理
│ ├── routes/ # 路由注册
│ ├── utils/ # 通用工具函数
│ ├── types/ # 全局类型声明
│ ├── app.ts # 应用装配(中间件、路由)
│ └── server.ts # 进程入口,监听端口
├── tests/
├── dist/ # 编译产物(gitignore)
├── tsconfig.json
├── package.json
└── .nvmrc
这个结构的核心原则是依赖方向单向:controllers依赖services,services依赖repositories,repositories依赖models。反向依赖(如service直接引用controller)应被视为架构违规。这样分层后,业务逻辑可以脱离HTTP框架独立测试,数据访问也可以替换实现而不影响上层。
下面用一张结构图展示各层之间的依赖方向与关键职责:
图中实线箭头表示运行时调用方向,虚线箭头表示装配或引用关系。可以看到,server.ts只负责启动,app.ts负责装配,真正的业务调用链是routes → controllers → services → repositories。每一层只依赖其下一层,models作为被各层共享的类型定义,不依赖任何其他层。
五、初始化后的验证
完成上述配置后,用以下步骤验证项目骨架是否可用:
- 执行
pnpm install安装依赖,确认lockfile生成且无报错。 - 在
src/server.ts写入一个最小HTTP服务,执行pnpm tsc --noEmit确认类型检查通过。 - 执行
pnpm tsc编译到dist/,检查产物目录结构与rootDir一致。 - 运行
node dist/server.js,用curl http://localhost:3000/health确认服务响应正常。 - 故意在代码中引用一个未声明的依赖,确认pnpm严格模式下会报错,验证幽灵依赖防护生效。
这五步覆盖了依赖安装、类型检查、编译输出、运行时执行和依赖隔离五个关键环节。任何一步失败,都说明初始化配置存在需要修正的问题,应在编写业务代码之前解决。
@/在编译后不会自动转换。如果使用ESM + NodeNext,需要在package.json中配置imports字段,或引入tsc-alias在编译后重写路径。否则运行时会报ERR_MODULE_NOT_FOUND。