1. TypeScript 类型系统
TypeScript 的类型系统是其区别于普通 JavaScript 的核心竞争力。在 Node.js 后端开发中,充分利用类型系统可以在编译阶段捕获大量潜在错误,减少运行时异常,同时为 IDE 提供精准的自动补全与重构能力。本节从实战角度梳理后端开发中最常用的高级类型工具。
1.1 关键类型工具
接口(interface)与类型别名(type)是定义数据结构的基础。接口支持声明合并,更适合描述对象形状;类型别名则更灵活,可表达联合类型、交叉类型与元组。在大型项目中,通常用接口定义实体,用类型别名定义 DTO 与 API 响应变体。
泛型(Generic)使得函数与类可以在不牺牲类型安全的前提下处理多种数据类型。条件类型(Conditional Type)与映射类型(Mapped Type)则是类型层面的逻辑运算,能够实现自动化类型转换,例如从数据库模型生成可选更新字段。
模板字面量类型(Template Literal Type)在构造类型安全的枚举路径时非常有用,比如 RESTful API 的路由参数。infer 关键字则用于在条件类型中提取子类型,是实现 ReturnType、Parameters 等内置工具类型的基石。
品牌类型(Branded Type)是一种名义类型系统模拟方案,通过在结构相同的基础类型上附加不可见的标记属性,防止不同业务语义 ID 的意外混用。
// 接口与类型别名
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
type UserInput = Omit<User, 'id'>;
type UserUpdate = Partial<User>;
type UserResponse = Pick<User, 'id' | 'name'>;
// 泛型函数
async function fetchApi<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
}
const user = await fetchApi<User>('/api/users/1');
// 条件类型
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<123>; // false
// 映射类型
type ReadonlyUser = { readonly [K in keyof User]: User[K] };
type NullableUser = { [K in keyof User]: User[K] | null };
// 模板字面量类型
type ApiPath = `/api/users/${string}` | `/api/orders/${number}`;
const validPath: ApiPath = '/api/users/42';
// 使用 infer 提取 Promise 内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type UserType = UnwrapPromise<Promise<User>>; // User
// 品牌类型:防止 UserId 与 OrderId 混用
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
function findUser(id: UserId): User {
// 即使都是 string,也不能传入 OrderId
return { id: id as unknown as string, name: 'Alice', email: 'a@x.com', role: 'user' } as User;
}
1.2 严格模式配置
tsconfig.json 是 TypeScript 项目的灵魂配置。启用 strict: true 会同时开启 noImplicitAny、strictNullChecks、strictFunctionTypes、strictBindCallApply、strictPropertyInitialization、noImplicitThis 与 alwaysStrict。对于后端服务,强烈建议开启严格模式,因为 null 与 undefined 引发的异常在生产环境中往往是致命的。moduleResolution 设为 NodeNext 与 module 对齐,确保 ESM 与 CommonJS 的互操作行为与 Node.js 原生保持一致。
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"noUncheckedIndexedAccess": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
2. Clean Architecture
Clean Architecture 由 Robert C. Martin 提出,其核心思想是依赖关系必须始终向内指向核心业务逻辑。外层的基础设施细节(数据库、Web 框架、第三方 SDK)可以轻易替换,而内核的实体与业务规则保持纯净,不依赖任何外部库。这种分层架构在 TypeScript 项目中通过目录强制约定与接口隔离原则得以落地。
2.1 目录结构与依赖规则
四层结构是 Clean Architecture 在 Node.js 中的常见落地形式。domain 层存放实体与仓库接口,application 层编排用例,infrastructure 层实现具体技术细节,main.ts 作为组合根(Composition Root)完成依赖装配。任何层的代码都不允许导入更外层的模块,这一规则可通过 ESLint 的 import/no-restricted-paths 规则强制执行。
src/
├── domain/ # 核心业务逻辑(不依赖外部)
│ ├── entities/ # 实体:包含业务规则与校验
│ └── repositories/ # 仓库接口:定义数据访问契约
│
├── application/ # 应用层
│ ├── services/ # 业务服务 / 用例
│ ├── dto/ # 数据传输对象
│ └── ports/ # 输入/输出端口接口
│
├── infrastructure/ # 基础设施
│ ├── db/ # 数据库实现(Prisma / TypeORM / Knex)
│ ├── http/ # 路由、控制器、中间件
│ ├── cache/ # Redis / Memcached 实现
│ └── container.ts # 依赖注入容器
│
└── main.ts # 入口,组装依赖并启动服务
2.2 接口实现与依赖注入
通过面向接口编程,应用层只依赖 domain 中定义的抽象接口,而具体实现由基础设施层提供。这种模式使得单元测试极为便捷——只需为仓库接口提供一个内存实现即可隔离测试业务逻辑。依赖注入容器负责解析接口与实现的映射关系,避免在业务代码中出现 new 关键字导致的硬编码依赖。
// domain/entities/user.entity.ts
export class User {
constructor(
public readonly id: string,
public name: string,
public email: string,
public role: 'admin' | 'user' | 'guest'
) {
if (!email.includes('@')) throw new Error('Invalid email');
}
toPrisma() {
return { id: this.id, name: this.name, email: this.email, role: this.role };
}
}
// domain/repositories/user.repository.interface.ts
export interface IUserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
}
// application/services/create-user.usecase.ts
export class CreateUserUseCase {
constructor(private userRepo: IUserRepository) {}
async execute(input: { name: string; email: string }): Promise<User> {
const id = crypto.randomUUID();
const user = new User(id, input.name, input.email, 'user');
await this.userRepo.save(user);
return user;
}
}
// infrastructure/db/prisma-user.repository.ts
export class PrismaUserRepository implements IUserRepository {
constructor(private prisma: PrismaClient) {}
async findById(id: string): Promise<User | null> {
const user = await this.prisma.user.findUnique({ where: { id } });
return user ? new User(user.id, user.name, user.email, user.role as User['role']) : null;
}
async save(user: User): Promise<void> {
await this.prisma.user.upsert({
where: { id: user.id },
update: user.toPrisma(),
create: user.toPrisma()
});
}
async delete(id: string): Promise<void> {
await this.prisma.user.delete({ where: { id } });
}
}
// infrastructure/http/user.controller.ts
export class UserController {
constructor(private createUser: CreateUserUseCase) {}
async create(req: Request, res: Response) {
const user = await this.createUser.execute(req.body);
res.status(201).json(user);
}
}
// main.ts:组合根
const prisma = new PrismaClient();
const userRepo = new PrismaUserRepository(prisma);
const createUserUseCase = new CreateUserUseCase(userRepo);
const userController = new UserController(createUserUseCase);
// 注册路由并启动服务器...
3. Docker 多阶段构建
Docker 多阶段构建(Multi-stage Build)能够在单一 Dockerfile 中定义多个构建阶段,每个阶段拥有独立的文件系统上下文。最终镜像仅保留生产运行所必需的最小文件集,从而显著降低攻击面与镜像体积。对于 TypeScript 项目,典型的两阶段构建分别为编译阶段(builder)与运行阶段(production)。
3.1 多阶段 Dockerfile 设计
在 builder 阶段,我们安装所有依赖(包括 devDependencies),挂载源码与配置,执行 tsc 编译。在 production 阶段,仅复制 package*.json 并执行 npm ci --only=production,这将跳过 TypeScript、测试框架等开发依赖的安装,最终镜像大小可压缩 60% 以上。使用 node:20-alpine 作为基础镜像进一步减少体积,但需注意 Alpine 使用 musl libc,若项目依赖包含原生 C++
扩展,应改用 node:20-slim。USER node 指令确保容器不以 root 身份运行进程,降低提权风险。健康检查(HEALTHCHECK)通过定期向应用端点发送请求,让 Docker 或编排系统能够自动重启无响应容器。
# 构建阶段
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build
# 生产阶段
FROM node:20-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
# 安全:创建非 root 用户
RUN addgroup -g 1001 -S nodejs && adduser -S node -u 1001
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => r.statusCode === 204 ? process.exit(0) : process.exit(1))"
CMD ["node", "dist/main.js"]
3.2 构建上下文优化
.dockerignore 文件与 .gitignore 类似,用于排除不需要发送至 Docker 守护进程的文件。合理配置可显著加快构建速度并减少缓存失效。应排除源码中的测试文件、本地环境配置与日志输出,同时保留 package-lock.json 以确保依赖安装的确定性复现。
node_modules
npm-debug.log
dist
.env
.env.local
.git
.vscode
coverage
*.test.ts
*.spec.ts
4. Error Handling 最佳实践
Node.js 的异步特性使得错误处理比同步代码更加复杂。未经捕获的 Promise 异常或抛出的 Error 对象一旦冒泡至进程级别,可能导致服务崩溃。工程化的错误处理体系应区分预期异常(如参数校验失败)与意外异常(如数据库连接断开),并以一致的 JSON 结构返回给调用方,便于前端与监控系统解析。
4.1 自定义错误类层级
通过继承原生 Error 类构建层级化的自定义错误体系,可在不依赖字符串匹配的前提下精确判断异常类型。基类 AppError 包含 HTTP 状态码、错误码与是否可暴露给客户端的标识。子类分别对应不同的业务异常场景,例如资源不存在、权限不足、数据冲突等。
export class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number,
public readonly isOperational: boolean = true
) {
super(message);
Object.setPrototypeOf(this, AppError.prototype);
Error.captureStackTrace(this, this.constructor);
}
}
export class ValidationError extends AppError {
constructor(message: string) {
super(message, 'VALIDATION_ERROR', 400);
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`, 'NOT_FOUND', 404);
}
}
export class ConflictError extends AppError {
constructor(message: string) {
super(message, 'CONFLICT', 409);
}
}
export class UnauthorizedError extends AppError {
constructor(message: string = 'Unauthorized') {
super(message, 'UNAUTHORIZED', 401);
}
}
4.2 全局异常处理中间件
在 Express 或 Fastify 中注册全局异常处理中间件,作为请求处理管道的最后一道防线。该中间件负责统一格式化错误响应:对于可预期的业务异常,返回包含错误码的结构化 JSON;对于不可预期的系统异常,则记录详细堆栈并返回模糊的 Internal Server Error,避免暴露内部实现细节。
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../errors/app.error';
export function errorHandler(
err: Error,
_req: Request,
res: Response,
_next: NextFunction
): void {
if (err instanceof AppError) {
res.status(err.statusCode).json({
success: false,
code: err.code,
message: err.isOperational ? err.message : 'Internal Server Error'
});
return;
}
// 未预料的异常:记录后返回 500
console.error('Unexpected error:', err);
res.status(500).json({
success: false,
code: 'INTERNAL_ERROR',
message: 'Internal Server Error'
});
}
4.3 Zod 验证错误转换
Zod 是 TypeScript 优先的 Schema 验证库,能够在运行时对传入数据进行类型检查。当验证失败时,Zod 抛出 ZodError,其中包含详细的字段级失败信息。为了避免将 Zod 的内部数据结构直接暴露给 API 消费者,应在控制器层捕获 ZodError 并将其转换为统一的 ValidationError。
import { z } from 'zod';
import { Request, Response, NextFunction } from 'express';
import { ValidationError } from '../errors/app.error';
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(18).optional()
});
export function validateBody<T extends z.ZodTypeAny>(schema: T) {
return (req: Request, _res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
const issues = result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ');
return next(new ValidationError(issues));
}
req.body = result.data;
next();
};
}
// 路由使用
// router.post('/users', validateBody(CreateUserSchema), userController.create);
5. 日志体系:Pino 结构化日志与全链路追踪
日志是后端系统可观测性的三大支柱之一(其余为指标与追踪)。相比于 console.log,结构化日志以 JSON 形式输出,便于 Logstash、Fluentd、Datadog 等日志收集器解析与索引。Pino 是 Node.js 生态中性能最优的日志库之一,其核心理念是最小化日志序列化开销,在开发环境提供可读格式,在生产环境输出紧凑 JSON。
5.1 Pino 结构化日志配置
通过 pino 与 pino-pretty 的配合,可以实现环境自适应的日志格式。生产环境启用 redact 配置自动脱敏敏感字段(如密码、Token),防止日志泄露导致的安全隐患。日志级别应支持通过环境变量动态调整,便于排查问题时临时开启 debug 输出。
import pino from 'pino';
const isDev = process.env.NODE_ENV !== 'production';
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
redact: {
paths: ['password', '*.password', 'token', 'authorization'],
remove: true
},
transport: isDev
? {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname'
}
}
: undefined
});
// 使用示例
logger.info({ userId: 'u-123' }, 'User login succeeded');
logger.error({ err: new Error('DB timeout') }, 'Database query failed');
5.2 request-id 全链路追踪
在微服务与异步事件驱动的架构中,单次用户请求可能跨越多个服务、数据库与消息队列。没有统一标识的日志就像散落的拼图,难以串联。通过在请求入口处生成 request-id 并将其注入 AsyncLocalStorage,后续的日志输出、数据库查询与 HTTP 调用均可自动携带该标识,实现全链路追踪。
import { AsyncLocalStorage } from 'async_hooks';
import { randomUUID } from 'crypto';
import { Request, Response, NextFunction } from 'express';
import { logger } from './logger';
export const asyncLocalStorage = new AsyncLocalStorage<{ requestId: string }>();
export function requestIdMiddleware(req: Request, res: Response, next: NextFunction): void {
const requestId = (req.headers['x-request-id'] as string) || randomUUID();
res.setHeader('x-request-id', requestId);
asyncLocalStorage.run({ requestId }, () => {
logger.info({ method: req.method, path: req.path, requestId }, 'Incoming request');
next();
});
}
// 获取当前上下文的子 logger
export function getContextLogger() {
const store = asyncLocalStorage.getStore();
return store ? logger.child({ requestId: store.requestId }) : logger;
}
// 服务层使用
export async function fetchUser(id: string) {
const log = getContextLogger();
log.debug({ userId: id }, 'Fetching user from database');
// ... database call
}
6. 测试策略:Vitest 单元测试与 Supertest 集成测试
测试是工程化体系中保障代码质量的最后一道防线,但并非唯一防线。合理的测试策略应当呈金字塔结构:底层是大量快速、廉价的单元测试,中层是覆盖关键路径的集成测试,顶层是少量的端到端测试。Vitest 作为 Vite 生态的测试运行器,对 TypeScript 提供原生支持(无需 ts-jest),测试启动速度比 Jest 快一个数量级。
6.1 Vitest 单元测试配置
单元测试的核心原则是隔离性:被测模块的依赖应当全部替换为测试替身(test double)。通过 vi.mock 自动模拟模块导入,或通过 IDE Repository 注入内存实现,可以确保单元测试不触及真实数据库或外部 API,从而保持毫秒级执行速度。覆盖率阈值通过 coverage.thresholds 强制执行,防止新代码未经测试便合入主干。
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80
}
}
}
});
6.2 测试替身与用例测试
// tests/unit/create-user.usecase.spec.ts
import { describe, it, expect, vi } from 'vitest';
import { CreateUserUseCase } from '../../src/application/services/create-user.usecase';
import { IUserRepository } from '../../src/domain/repositories/user.repository.interface';
const mockRepo: IUserRepository = {
findById: vi.fn(),
save: vi.fn().mockResolvedValue(undefined),
delete: vi.fn()
};
describe('CreateUserUseCase', () => {
it('should create a user with generated id', async () => {
const useCase = new CreateUserUseCase(mockRepo);
const user = await useCase.execute({ name: 'Alice', email: 'alice@x.com' });
expect(user.name).toBe('Alice');
expect(user.email).toBe('alice@x.com');
expect(user.role).toBe('user');
expect(mockRepo.save).toHaveBeenCalledTimes(1);
});
it('should throw for invalid email', async () => {
const useCase = new CreateUserUseCase(mockRepo);
await expect(
useCase.execute({ name: 'Bob', email: 'not-an-email' })
).rejects.toThrow('Invalid email');
});
});
6.3 Supertest 集成测试
集成测试验证多个组件协同工作的正确性,通常以真实 HTTP 请求的形式驱动应用。Supertest 能够挂载 Express/Fastify 应用实例并发送请求,无需真正监听网络端口。集成测试的数据库状态应通过 beforeEach / afterEach 进行回滚或重置,确保测试之间互不干扰。
// tests/integration/user.routes.spec.ts
import request from 'supertest';
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createApp } from '../../src/main';
const app = createApp();
describe('POST /users', () => {
it('should create a user and return 201', async () => {
const res = await request(app)
.post('/users')
.send({ name: 'Charlie', email: 'charlie@x.com' })
.set('Accept', 'application/json');
expect(res.status).toBe(201);
expect(res.body).toHaveProperty('id');
expect(res.body.name).toBe('Charlie');
});
it('should return 400 for invalid payload', async () => {
const res = await request(app)
.post('/users')
.send({ name: '', email: 'bad' });
expect(res.status).toBe(400);
expect(res.body.code).toBe('VALIDATION_ERROR');
});
});
7. 配置管理:环境变量与运行时验证
现代 Node.js 应用通常部署在多个环境(开发、测试、预发布、生产),每个环境的配置项(数据库连接串、API 密钥、日志级别)各不相同。将配置硬编码在源码中是不可接受的工程实践。将配置完全依赖环境变量虽然灵活,但缺少类型安全与运行时校验,一旦某个必填变量缺失,问题只能在运行时暴露。
7.1 分层加载与 dotenv
dotenv 允许从 .env 文件加载环境变量,但不应将其提交至版本控制。通过分层加载策略,.env 提供默认值,.env.local 提供本地覆盖,而生产环境则完全依赖平台注入的系统环境变量。加载顺序至关重要:本地覆盖文件应当在最后加载,以取得最高优先级。
import { config } from 'dotenv';
import { resolve } from 'path';
const env = process.env.NODE_ENV || 'development';
const envFiles = ['.env', `.env.${env}`, '.env.local'];
for (const file of envFiles) {
config({ path: resolve(process.cwd(), file), override: true });
}
7.2 运行时配置验证
env-var 与 Zod 的组合能够在应用启动时一次性验证所有环境变量,失败即崩溃(fail fast),避免携带无效配置运行。定义一个集中式的 Config 对象,将原始字符串转换为适当的类型(数字、布尔值、枚举、URL)。其他业务模块只导入 config 对象,不再直接访问 process.env。
import { z } from 'zod';
const configSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.string().transform(Number).default('3000'),
DATABASE_URL: z.string().url(),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
JWT_SECRET: z.string().min(32),
REDIS_URL: z.string().url().optional()
});
const parsed = configSchema.safeParse(process.env);
if (!parsed.success) {
console.error('Invalid environment variables:', parsed.error.format());
process.exit(1);
}
export const config = parsed.data;
// 使用
// import { config } from './config';
// const port = config.PORT;
8. TypeScript ESM 与 CommonJS 互操作
Node.js 14 起正式支持 ESM, TypeScript 4.7 引入 .mts / .cts 扩展名以明确指定模块格式。然而,npm 生态中仍有大量包仅提供 CommonJS 导出,混合格式引发的 ERR_REQUIRE_ESM 与 Can't use import statement 错误是 TS Node 项目中最令人头疼的构建问题之一。
8.1 模块格式与扩展名语义
TypeScript 编译后的文件扩展名由源文件扩展名与 tsconfig 配置共同决定:
| 源文件扩展名 | module 目标 | 输出扩展名 | 语义 |
|---|---|---|---|
.ts | CommonJS | .js | CJS(当 type: commonjs) |
.ts | ESM | .js | ESM(当 type: module) |
.mts | 任意 | .mjs | 强制 ESM |
.cts | 任意 | .cjs | 强制 CJS |
在 package.json 中设置 "type": "module" 后,所有 .js 与 .ts 文件默认被视为 ESM,而 .cjs 与 .cts 则强制为 CommonJS。建议新项目直接采用 ESM,仅在迁移遗留工具脚本或依赖 CJS-only 包的特殊场景下使用 .cts 文件。
{
"name": "my-service",
"type": "module",
"main": "./dist/main.js",
"exports": {
".": {
"import": "./dist/main.js",
"require": "./dist/main.cjs"
}
}
}
8.2 混合格式项目最佳实践
当 ESM 项目需要引入仅支持 require 的 CJS 包时,Node.js 允许 ESM 使用 createRequire 合成模块导入。反之,CJS 通过动态 import() 加载 ESM 模块。TypeScript 编译选项 esModuleInterop 与 allowSyntheticDefaultImports 会减少大部分导入语法差异,但在运行时仍需关注模块格式的匹配。
// 在 ESM 中使用 createRequire 加载 CJS 配置
import { createRequire } from 'module';
import { fileURLToPath } from 'url';
const require = createRequire(import.meta.url);
const legacyConfig = require('./legacy.cjs');
// CJS 中动态导入 ESM
async function loadEsmModule() {
const { foo } = await import('./esm-module.mjs');
return foo;
}
9. Monorepo 支持:workspace 配置与共享类型
随着业务规模增长,后端服务往往分解为多个可独立部署的子服务,而工具库、类型定义与配置需要跨服务复用。Monorepo 架构将所有相关代码仓库放入单一版本控制树中,通过 Workspace 机制统一管理依赖与构建脚本,避免多仓库之间的版本漂移与重复CI配置。
9.1 pnpm Workspace 配置
pnpm 的 Workspace 特性是解决 Monorepo 依赖冗余的最佳方案之一:公共依赖(如 TypeScript、Zod)仅在仓库根目录安装一份,各子包通过软链接引用,大幅减少 node_modules 总体积。
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
// package.json(根)
{
"name": "my-platform",
"private": true,
"scripts": {
"build": "pnpm -r build",
"test": "pnpm -r test",
"lint": "pnpm -r lint"
},
"devDependencies": {
"typescript": "^5.5.0",
"vitest": "^2.0.0"
}
}
9.2 共享类型包与交叉引用
将核心业务类型(DTO、Entity、API 响应)抽取到独立的 @my-platform/types 包中,前后端均可依赖。每个子包在 package.json 的 exports 字段显式声明公开接口,配合 tsconfig 的 references 实现增量编译与 IntelliSense 跳转。
// packages/types/package.json
{
"name": "@my-platform/types",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./dist/index.js",
"./user": "./dist/user.js"
},
"typesVersions": {
"*": {
"*": ["./dist/*"]
}
}
}
// tsconfig.json(根)
{
"compilerOptions": {
"composite": true,
"declarationMap": true
},
"references": [
{ "path": "./packages/types" },
{ "path": "./apps/api" }
]
}
// apps/api/src/services/user.service.ts
import { CreateUserDto, UserResponse } from '@my-platform/types/user';
export async function createUser(dto: CreateUserDto): Promise<UserResponse> {
// 业务逻辑...
}
通过在根目录统一配置 ESLint、Prettier 与 TypeScript 标准,团队可以在新增服务时复用已有工程化基建,显著降低 Marginal Cost(边际成本),保持技术栈的一致性。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。