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

包管理器决定了依赖安装速度、磁盘占用和依赖树的确定性。三者的核心差异如下:

维度npmpnpmyarn (Berry)
依赖存储方式扁平化 node_modules全局内容寻址存储 + 硬链接PnP 或 node_modules
磁盘占用高(重复安装)低(多项目共享)中
幽灵依赖存在严格隔离,基本消除PnP 模式下消除
安装速度中等快快
Monorepo 支持workspacesworkspaces(体验最佳)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——生成类型声明和源码映射,便于调试和作为库被引用。
如果项目使用CJS,则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框架独立测试,数据访问也可以替换实现而不影响上层。

下面用一张结构图展示各层之间的依赖方向与关键职责:

TypeScript 后端项目分层结构与依赖方向 server.ts 进程入口 · 端口监听 · 优雅退出 app.ts 中间件注册 · 路由挂载 · 错误处理 routes/ URL → Controller 映射 controllers/ 参数解析 · 响应组装 services/ 业务逻辑 · 事务编排 repositories/ 数据访问 · SQL/ORM 封装 models/ 领域模型 · 类型定义 config/ 环境变量 · 启动校验 middlewares/ 认证 · 日志 · 异常捕获 utils/ 通用工具 · 无业务依赖 types/ 全局类型声明 依赖方向:上层 → 下层,禁止反向依赖

图中实线箭头表示运行时调用方向,虚线箭头表示装配或引用关系。可以看到,server.ts只负责启动,app.ts负责装配,真正的业务调用链是routes → controllers → services → repositories。每一层只依赖其下一层,models作为被各层共享的类型定义,不依赖任何其他层。

五、初始化后的验证

完成上述配置后,用以下步骤验证项目骨架是否可用:

  1. 执行pnpm install安装依赖,确认lockfile生成且无报错。
  2. 在src/server.ts写入一个最小HTTP服务,执行pnpm tsc --noEmit确认类型检查通过。
  3. 执行pnpm tsc编译到dist/,检查产物目录结构与rootDir一致。
  4. 运行node dist/server.js,用curl http://localhost:3000/health确认服务响应正常。
  5. 故意在代码中引用一个未声明的依赖,确认pnpm严格模式下会报错,验证幽灵依赖防护生效。

这五步覆盖了依赖安装、类型检查、编译输出、运行时执行和依赖隔离五个关键环节。任何一步失败,都说明初始化配置存在需要修正的问题,应在编写业务代码之前解决。

路径别名@/在编译后不会自动转换。如果使用ESM + NodeNext,需要在package.json中配置imports字段,或引入tsc-alias在编译后重写路径。否则运行时会报ERR_MODULE_NOT_FOUND。