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和TS+Express的开发体验:
| 维度 | JavaScript + Express | TypeScript + Express |
|---|---|---|
| 类型安全 | 运行时才能发现错误 | 编码时即时提示 |
| 代码维护性 | 重构困难 | 智能重命名和跳转 |
| 开发效率 | 需要频繁console.log | 完善的类型推导 |
| 团队协作 | 依赖文档约定 | 接口即文档 |
2. 环境准备与项目初始化
2.1 Node.js环境配置
我强烈推荐使用nvm(Node Version Manager)来管理Node版本。特别是在团队协作时,它能确保所有成员使用相同的Node版本:
# 安装nvm(Windows用户请使用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.json:
mkdir 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.ts:
import { 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());集群模式:利用多核CPU
import 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); }缓存策略:添加ETag
app.set('etag', 'strong');
5.2 Docker化部署
创建Dockerfile:
FROM node:18-alpine WORKDIR /usr/src/app COPY package*.json ./ RUN npm ci --only=production 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配置,开发环境可以使用cors:
import 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.ts):
import { 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.ts
- services/user.service.ts
- models/user.model.ts
- dtos/create-user.dto.ts
- validations/user.validation.ts
这种结构虽然初期看起来复杂,但在项目规模扩大后,维护成本反而会降低。我们有个项目从3万行代码增长到15万行,依然保持可维护性,关键就在于严格遵循这种架构。