用 TypeScript 编写 Node.js 后端可以大幅提升可维护性和开发效率。静态类型能够在编译期捕获大量潜在错误,配合现代编辑器的光速级 IntelliSense,团队协作与代码重构的自信心显著增强。本文覆盖 TS + Node 的工程化全流程,从项目初始化到生产部署,力求提供一份可直接落地的实践指南。
一、项目初始化
推荐采用以下目录结构,将源码、测试、配置分离,方便后续扩展与维护:
my-api/
├── src/
│ ├── index.ts
│ ├── routes/
│ ├── controllers/
│ ├── services/
│ └── types/
├── tests/
├── dist/
├── prisma/
├── tsconfig.json
├── nodemon.json
└── package.json
使用以下命令初始化项目并安装核心依赖:
mkdir my-api && cd my-api
npm init -y
npm install -D typescript ts-node nodemon @types/node
npx tsc --init
tsconfig.json 是 TypeScript 项目的灵魂,建议采用如下配置,兼顾严格性与开发体验:
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"baseUrl": ".",
"paths": { "@/*": ["src/*"] },
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}
其中 paths 与 baseUrl 配合使用,可实现 @/services/user 这般优雅的路径别名,彻底告别 ../../../ 地狱。strict: true 开启全部严格类型检查,declaration 选项则便于后续作为库发布时自动生成 .d.ts 声明文件。
二、Express + TS
Express 在 JavaScript 生态中拥有最为广泛的中间件资源,配合 TypeScript 后,路由与中间件的类型安全得以保障。
首先需要扩展 Express 的 Request 类型,以便在中间件中注入自定义属性(例如认证后的用户信息):
// src/types/express.d.ts
import { User } from './user';
declare global {
namespace Express {
interface Request {
user?: User;
}
}
}
接着构建类型安全的路由处理函数。对于异步路由,务必封装 asyncHandler 以避免未捕获的 Promise 异常导致进程崩溃:
// src/utils/asyncHandler.ts
import { Request, Response, NextFunction } from 'express';
export const asyncHandler = (
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// src/routes/user.route.ts
import { Router, Request, Response } from 'express';
import { asyncHandler } from '../utils/asyncHandler';
interface User {
id: number;
name: string;
email: string;
}
const router = Router();
router.get(
'/users/:id',
asyncHandler(async (req: Request<{ id: string }>, res: Response<User>) => {
const user: User = {
id: Number(req.params.id),
name: 'Alice',
email: 'alice@example.com',
};
res.json(user);
})
);
export default router;
Request<{ id: string }> 为路由参数提供泛型约束,Response<User> 则约束了响应体的结构。当尝试返回不符合 User 接口的对象时,TypeScript 编译器会立即报错。
三、Nest.js(推荐企业级)
Nest.js 原生基于 TypeScript 设计,是 Node.js 的 Angular 式框架。它将应用拆分为模块(Module)、控制器(Controller)与服务(Service),依赖注入容器贯穿始终,天然适合大型团队与复杂业务。
初始化项目非常简单:
npm install -g @nestjs/cli
nest new my-project
Nest.js 的核心在于装饰器与依赖注入。以下示例展示了完整的用户模块,包括 DTO(数据传输对象)、控制器与服务三层分离:
// src/users/dto/create-user.dto.ts
export class CreateUserDto {
readonly name: string;
readonly email: string;
}
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable()
export class UsersService {
private users: User[] = [];
create(dto: CreateUserDto): User {
const user = { id: Date.now(), ...dto };
this.users.push(user);
return user;
}
findById(id: string): User | undefined {
return this.users.find((u) => u.id === Number(id));
}
}
// src/users/users.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { UsersService, User } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
async create(@Body() dto: CreateUserDto): Promise<User> {
return this.usersService.create(dto);
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<User | undefined> {
return this.usersService.findById(id);
}
}
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
@Injectable() 将 UsersService 注册到 Nest 的 IoC 容器,@Module 负责声明该模块的边界与导出。借助管道(Pipe)、守卫(Guard)和拦截器(Interceptor),还能在框架层面统一处理参数校验、权限校验与响应转换。
四、开发工具链
高效的开发工具链是工程化的基石。热重载方面,推荐使用 nodemon 配合 ts-node 的 transpileOnly 模式,以牺牲部分类型检查为代价换取更快的重启速度,类型问题交由 IDE 实时提示:
// nodemon.json
{
"watch": ["src"],
"ext": "ts",
"ignore": ["src/**/*.spec.ts"],
"exec": "ts-node --transpile-only src/index.ts"
}
在 package.json 中配置常用脚本:
{
"scripts": {
"dev": "nodemon",
"build": "tsc",
"start": "node dist/index.js",
"test": "jest",
"test:watch": "jest --watch"
}
}
测试框架采用 Jest 搭配 ts-jest。为了让 Jest 识别路径别名,需要显式配置 moduleNameMapper:
// jest.config.ts
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/tests'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'],
};
export default config;
// tests/user.service.spec.ts
import { UsersService } from '../src/users/users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(() => {
service = new UsersService();
});
it('should create a user', () => {
const user = service.create({ name: 'Bob', email: 'bob@example.com' });
expect(user.name).toBe('Bob');
});
});
五、Prisma 类型安全 ORM
Prisma 是当前 Node.js 生态中类型安全体验最佳的 ORM。它通过声明式 Schema 定义数据模型,再自动生成包含完整 TypeScript 类型的客户端,彻底告别手写模型类型与 SQL 拼接。
首先在 prisma/schema.prisma 中定义模型:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
createdAt DateTime @default(now())
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
执行迁移与客户端生成:
npx prisma migrate dev --name init
npx prisma generate
在业务代码中,PrismaClient 的所有查询方法均返回精确推断的类型:
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error'],
});
// 完全类型安全的查询
const userWithPosts = await prisma.user.findUnique({
where: { id: 1 },
include: { posts: true },
});
// userWithPosts 的类型自动推断为 User & { posts: Post[] }
// 类型安全的创建,字段缺失或类型不符将立即报错
const newUser = await prisma.user.create({
data: {
email: 'alice@example.com',
name: 'Alice',
posts: {
create: [{ title: 'Hello Prisma', published: true }],
},
},
});
// 事务保证原子性
const [updatedUser, updatedPost] = await prisma.$transaction([
prisma.user.update({ where: { id: 1 }, data: { name: 'Updated' } }),
prisma.post.update({ where: { id: 1 }, data: { published: true } }),
]);
Prisma 的 include 与 select 选项会根据 Schema 关系自动扩展返回类型,这对于复杂的多表联查场景尤为宝贵,开发者无需再为联查结果手写冗长的接口定义。
六、Zod 运行时类型校验与 OpenAPI 生成
TypeScript 的静态类型只在编译期生效,运行时收到的请求数据仍然可能是恶意或畸形的。Zod 是一个以 TypeScript 优先的 Schema 校验库,通过 .parse() 或 .safeParse() 在运行时守护数据边界。
定义通用校验 Schema 并导出推断类型:
// src/schemas/user.schema.ts
import { z } from 'zod';
export const CreateUserSchema = z.object({
name: z.string().min(1, '用户名不能为空'),
email: z.string().email('邮箱格式不正确'),
age: z.number().int().min(0).optional(),
});
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
在 Express 中封装校验中间件:
// src/middlewares/validate.ts
import { Request, Response, NextFunction } from 'express';
import { ZodSchema, ZodError } from 'zod';
export const validate =
(schema: ZodSchema) => (req: Request, res: Response, next: NextFunction) => {
try {
schema.parse(req.body);
next();
} catch (error) {
if (error instanceof ZodError) {
return res.status(400).json({
message: '参数校验失败',
errors: error.flatten().fieldErrors,
});
}
next(error);
}
};
路由中直接复用:
import { Router } from 'express';
import { validate } from '../middlewares/validate';
import { CreateUserSchema } from '../schemas/user.schema';
const router = Router();
router.post('/users', validate(CreateUserSchema), (req, res) => {
// req.body 已通过校验,可安全使用
res.json({ success: true, data: req.body });
});
借助 @asteasolutions/zod-to-openapi,Zod Schema 可直接驱动 OpenAPI 文档生成:
// src/openapi/registry.ts
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
extendZodWithOpenApi(z);
export const registry = new OpenAPIRegistry();
export const UserSchema = z
.object({
id: z.number().openapi({ example: 1 }),
name: z.string().openapi({ example: 'Alice' }),
email: z.string().openapi({ example: 'alice@example.com' }),
})
.openapi('User');
registry.registerPath({
method: 'get',
path: '/users/{id}',
request: { params: z.object({ id: z.string() }) },
responses: {
200: {
description: '获取用户成功',
content: { 'application/json': { schema: UserSchema } },
},
},
});
将生成的 OpenAPI 对象喂给 swagger-ui-express,即可在 /api-docs 路径获得交互式文档,前后端接口契约由此统一。
七、Monorepo 工程化:pnpm workspace + Turborepo + Changesets
当项目规模扩大,或者存在共享的公用库(如 @myapp/types、@myapp/utils)时,Monorepo 是更优的组织形式。pnpm 的 workspace 机制配合 Turborepo 的任务编排与远程缓存,可显著加速构建流程。
首先创建 pnpm-workspace.yaml:
packages:
- 'packages/*'
- 'apps/*'
目录结构示意:
my-repo/
├── packages/
│ ├── shared-types/
│ └── logger/
├── apps/
│ ├── api/
│ └── web/
├── turbo.json
└── package.json
turbo.json 中配置任务依赖与缓存策略:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"]
},
"lint": {},
"dev": {
"cache": false,
"persistent": true
}
}
}
dependsOn: ["^build"] 表示当前包的 build 任务依赖上游 workspace 包的 build 先完成,Turborepo 会自动分析依赖拓扑并按正确顺序执行。构建产物通过远程缓存共享后,团队成员与 CI 环境只需下载缓存即可跳过重复编译。
使用 Changesets 管理版本号与 CHANGELOG:
pnpm add -D @changesets/cli
npx changeset init
开发者在修改包后执行 npx changeset 选择变更集,CI 中运行 changeset version 自动提升版本并生成 CHANGELOG,changeset publish 则负责批量发布到 npm 私有仓库。整个流程规范了多包版本演进,避免了手动修改版本号带来的遗漏与冲突。
八、ESLint + Prettier + Husky 代码质量保证
代码风格的统一不仅关乎阅读体验,更是减少不必要代码审阅噪音的关键。采用 ESLint Flat Config 新格式,可将所有配置收敛到单一文件:
// eslint.config.js
import js from '@eslint/js';
import tsEslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';
export default tsEslint.config(
js.configs.recommended,
...tsEslint.configs.recommended,
{
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/explicit-function-return-type': 'off',
'no-console': 'warn',
},
},
prettier
);
Prettier 专注于格式化,避免与 ESLint 规则冲突:
// prettier.config.js
export default {
semi: true,
singleQuote: true,
trailingComma: 'es5',
printWidth: 100,
tabWidth: 2,
};
Husky 与 lint-staged 配合,在代码提交前自动执行格式修正与静态检查:
pnpm add -D husky lint-staged
npx husky init
// lint-staged.config.js
export default {
'*.{ts,tsx,js,jsx}': ['eslint --fix', 'prettier --write'],
};
# .husky/pre-commit
npx lint-staged
提交前的钩子保证了进入仓库的每一行代码都符合规范,历史记录的整洁度由此得到长期维护。
九、统一错误处理与日志体系(自定义 ExceptionFilter + Pino/Winston)
在 Nest.js 中,自定义 ExceptionFilter 可以拦截应用内抛出的全部异常,输出标准化的错误响应:
// src/filters/http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Response } from 'express';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.getResponse()
: 'Internal server error';
response.status(status).json({
success: false,
statusCode: status,
message,
timestamp: new Date().toISOString(),
});
}
}
在主模块中全局注册:
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
},
],
})
export class AppModule {}
日志方面,Pino 以其极低的性能开销成为 Node.js 高性能服务的首选。通过 pino-pretty 在开发环境输出可读格式,生产环境则直接输出结构化 JSON:
// src/logger.ts
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport:
process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty', options: { colorize: true } }
: undefined,
redact: ['req.headers.authorization', 'password'],
});
在请求上下文中注入 request-id,实现全链路追踪:
import { v4 as uuidv4 } from 'uuid';
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] || uuidv4();
res.setHeader('x-request-id', req.id);
logger.child({ requestId: req.id }).info({ req }, 'incoming request');
next();
});
结构化日志结合 ELK 或 Loki 等日志收集系统,可以在分布式环境下快速定位问题根因。
十、Docker 多阶段构建与生产部署
多阶段构建(Multi-stage Build)是瘦身 Docker 镜像的黄金法则。首阶段基于完整 Node 镜像完成依赖安装与构建,二阶段仅复制构建产物与生产依赖,最终镜像体积可从数百 MB 压缩到百余 MB 以内。
# Dockerfile
# 阶段一:构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY tsconfig.json ./
RUN npm ci
COPY . .
RUN npm run build
# 阶段二:生产运行
FROM node:20-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => r.statusCode === 200 ? process.exit(0) : process.exit(1))"
CMD ["node", "dist/index.js"]
.dockerignore 文件同样不可忽视:
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md
.env
.env.local
.env.*.local
coverage
dist
tests
配合 [docker](/posts/docker/)-compose.yml 实现本地一键启动:
version: '3.8'
services:
api:
build: .
ports:
- '3000:3000'
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://user:pass@db:5432/mydb
- LOG_LEVEL=info
depends_on:
- db
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
生产部署时,推荐使用健康检查探针(如 Kubernetes 的 LivenessProbe 与 ReadinessProbe)配合上述 HEALTHCHECK 指令,确保容器在依赖就绪后才接受流量,异常时能被编排系统自动重启或摘除。
相关阅读
- TypeScript 详解
- Node.js 专题
- Docker 部署 Node.js
- TypeScript 专题导航
- Zod 官方文档
- Nest.js 官方文档
- Prisma 官方文档
- Turborepo 官方文档
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。