Express与TypeScript构建Node.js后端服务实战指南

Express与TypeScript构建Node.js后端服务实战指南 1. 为什么选择Express TypeScript开发后端服务在2023年的Node.js生态调研中Express依然以76.8%的使用率位居榜首数据来源State of JS 2023。而TypeScript的采用率也从2019年的21%飙升至现在的84%。这两个技术的组合正在成为现代Node.js后端开发的事实标准。我最初接触这个组合是在一个电商后台系统的重构项目中。当时我们的JavaScript代码已经增长到3万行类型错误导致的线上事故每月至少2-3起。迁移到TypeScript后第一个月就拦截了47个潜在运行时错误。下面这张表格对比了纯JS和TSExpress的开发体验维度JavaScript ExpressTypeScript Express类型安全运行时才能发现错误编码时即时提示代码维护性重构困难智能重命名和跳转开发效率需要频繁console.log完善的类型推导团队协作依赖文档约定接口即文档2. 环境准备与项目初始化2.1 Node.js环境配置我强烈推荐使用nvmNode Version Manager来管理Node版本。特别是在团队协作时它能确保所有成员使用相同的Node版本# 安装nvmWindows用户请使用nvm-windows curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash # 安装当前LTS版本的Node.js nvm install 18.16.0 nvm use 18.16.0注意很多初学者会忽略Node版本管理导致本地运行正常但服务器报错。我们曾经因为开发机用Node 16而生产环境用Node 14导致async/await异常。2.2 TypeScript配置创建项目目录后先初始化package.jsonmkdir express-ts-server cd express-ts-server npm init -y然后安装TypeScript及相关类型定义npm install typescript types/node --save-dev npx tsc --init生成的tsconfig.json需要特别关注这几个配置项{ compilerOptions: { target: ES2020, module: commonjs, outDir: ./dist, rootDir: ./src, strict: true, esModuleInterop: true, skipLibCheck: true } }3. Express核心架构搭建3.1 基础服务结构安装Express和其类型定义npm install express types/express创建src/app.ts作为入口文件import express, { Application, Request, Response } from express; const app: Application express(); const PORT process.env.PORT || 3000; // 中间件配置 app.use(express.json()); app.use(express.urlencoded({ extended: true })); // 健康检查路由 app.get(/health, (req: Request, res: Response) { res.status(200).json({ status: UP }); }); // 启动服务 app.listen(PORT, () { console.log(Server is running on http://localhost:${PORT}); });在package.json中添加启动脚本{ scripts: { build: tsc, start: node dist/app.js, dev: ts-node-dev --respawn src/app.ts } }3.2 路由分层设计Express的灵活度是把双刃剑。我见过太多把所有路由堆在app.ts里的面条代码。推荐采用分层架构src/ ├── controllers/ │ ├── user.controller.ts │ └── product.controller.ts ├── routes/ │ ├── user.routes.ts │ └── product.routes.ts ├── services/ │ └── user.service.ts └── app.ts以用户模块为例routes/user.routes.tsimport { Router } from express; import { getUser, createUser } from ../controllers/user.controller; const router Router(); router.get(/:id, getUser); router.post(/, createUser); export default router;然后在app.ts中统一注册路由import userRoutes from ./routes/user.routes; app.use(/api/users, userRoutes);4. 进阶开发技巧4.1 中间件开发实战Express的中间件是其核心特性。我们开发过一个性能监控中间件import { Request, Response, NextFunction } from express; export const responseTime ( req: Request, res: Response, next: NextFunction ) { const start Date.now(); res.on(finish, () { const duration Date.now() - start; console.log(${req.method} ${req.url} - ${duration}ms); }); next(); };使用时只需app.use(responseTime);4.2 错误处理最佳实践Express默认的错误处理很基础。这是我们团队打磨过的错误处理中间件import { Request, Response, NextFunction } from express; class AppError extends Error { statusCode: number; constructor(message: string, statusCode: number 400) { super(message); this.statusCode statusCode; } } export const errorHandler ( err: Error, req: Request, res: Response, next: NextFunction ) { if (err instanceof AppError) { return res.status(err.statusCode).json({ status: error, message: err.message }); } console.error(Unexpected error:, err); res.status(500).json({ status: error, message: Internal server error }); };在控制器中这样使用export const getUser async (req: Request, res: Response) { const user await UserService.findById(req.params.id); if (!user) { throw new AppError(User not found, 404); } res.json(user); };最后在app.ts末尾注册错误处理器app.use(errorHandler);5. 项目优化与部署5.1 性能调优技巧路由压缩使用compression中间件npm install compression types/compressionimport compression from compression; app.use(compression());集群模式利用多核CPUimport cluster from cluster; import os from os; if (cluster.isPrimary) { const cpuCount os.cpus().length; for (let i 0; i cpuCount; i) { cluster.fork(); } } else { app.listen(PORT); }缓存策略添加ETagapp.set(etag, strong);5.2 Docker化部署创建DockerfileFROM node:18-alpine WORKDIR /usr/src/app COPY package*.json ./ RUN npm ci --onlyproduction COPY . . RUN npm run build EXPOSE 3000 CMD [node, dist/app.js]构建并运行docker build -t express-ts-app . docker run -p 3000:3000 -d express-ts-app经验之谈在Dockerfile中分开COPY package.json和源代码可以充分利用Docker的缓存层机制大幅加快重复构建速度。6. 常见问题解决方案6.1 类型扩展问题当需要扩展Request对象时很多人会直接修改全局类型。更安全的方式是declare global { namespace Express { interface Request { user?: { id: string; role: string; }; } } }然后在认证中间件中export const authMiddleware ( req: Request, res: Response, next: NextFunction ) { const token req.headers.authorization?.split( )[1]; const user verifyToken(token); // 假设的验证函数 req.user user; next(); };6.2 热重载配置使用ts-node-dev实现保存即重载npm install ts-node-dev --save-devpackage.json配置{ scripts: { dev: ts-node-dev --respawn --transpile-only src/app.ts } }6.3 跨域处理生产环境应该通过Nginx配置开发环境可以使用corsimport cors from cors; app.use( cors({ origin: [http://localhost:3000, https://your-production-domain.com], methods: [GET, POST, PUT, DELETE], allowedHeaders: [Content-Type, Authorization] }) );7. 测试策略7.1 单元测试配置使用Jest测试框架npm install jest ts-jest types/jest --save-devjest.config.js配置module.exports { preset: ts-jest, testEnvironment: node, testMatch: [**/*.test.ts], modulePaths: [rootDir/src] };示例测试测试services/user.service.tsimport { getUser } from ../services/user.service; describe(User Service, () { it(should return user by id, async () { const mockUser { id: 1, name: Test User }; User.findById jest.fn().mockResolvedValue(mockUser); const user await getUser(1); expect(user).toEqual(mockUser); expect(User.findById).toHaveBeenCalledWith(1); }); });7.2 集成测试方案使用supertest进行API测试import request from supertest; import app from ../app; describe(User API, () { it(GET /api/users/:id - should return user, async () { const response await request(app) .get(/api/users/1) .expect(200); expect(response.body).toHaveProperty(id); expect(response.body.id).toBe(1); }); });8. 项目结构优化建议经过多个项目的迭代我们总结出这套生产级目录结构src/ ├── config/ # 环境配置 ├── constants/ # 常量定义 ├── controllers/ # 控制器 ├── dtos/ # 数据传输对象 ├── exceptions/ # 自定义异常 ├── interfaces/ # TypeScript接口 ├── middlewares/ # 中间件 ├── models/ # 数据库模型 ├── routes/ # 路由定义 ├── services/ # 业务逻辑 ├── utils/ # 工具函数 ├── validations/ # 验证规则 └── app.ts # 应用入口每个新模块都应该遵循这个结构。例如用户模块会包含controllers/user.controller.tsservices/user.service.tsmodels/user.model.tsdtos/create-user.dto.tsvalidations/user.validation.ts这种结构虽然初期看起来复杂但在项目规模扩大后维护成本反而会降低。我们有个项目从3万行代码增长到15万行依然保持可维护性关键就在于严格遵循这种架构。