Express.js + TypeScript 5.5 实战:3 种中间件类型定义与错误处理模式

在构建生产级 REST API 时,类型安全是确保代码健壮性的关键。Express.js 作为 Node.js 生态中最受欢迎的 Web 框架,与 TypeScript 的结合能够显著提升开发体验和代码质量。本文将深入探讨三种不同复杂度的中间件类型定义方法,并提供一个完整的类型化错误处理解决方案。

1. 基础环境配置与项目初始化

首先,我们需要创建一个支持 TypeScript 5.5 的 Express.js 项目。最新的 TypeScript 版本带来了更强大的类型推断和性能优化,特别适合大型后端项目。

# 初始化项目
mkdir express-ts-api && cd express-ts-api
npm init -y

# 安装核心依赖
npm install express
npm install --save-dev typescript@5.5 @types/express @types/node

# 创建基础目录结构
mkdir src
touch src/app.ts src/types.ts

配置 tsconfig.json 文件以启用最新的 TypeScript 特性:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

提示:TypeScript 5.5 改进了对装饰器的支持,这在定义高级中间件时非常有用。

2. 三种中间件类型定义模式

2.1 基础类型化中间件

最简单的中间件类型定义方式是利用 Express 自带的 RequestHandler 类型:

import { RequestHandler } from 'express';

// 基础日志中间件
const loggerMiddleware: RequestHandler = (req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
  next();
};

// 带参数校验的基础中间件
const validateParams: RequestHandler<{ id: string }> = (req, res, next) => {
  if (!/^\d+$/.test(req.params.id)) {
    return res.status(400).json({ error: 'ID must be numeric' });
  }
  next();
};

这种模式适合简单的中间件,但缺乏对请求体和响应体的精细控制。

2.2 泛型增强型中间件

对于需要处理特定类型请求体的场景,我们可以创建泛型中间件:

import { Request, Response, NextFunction } from 'express';

interface TypedRequestBody<T> extends Request {
  body: T;
}

export function createValidatorMiddleware<T>(
  schema: Zod.Schema<T>
): RequestHandler {
  return (req: TypedRequestBody<T>, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({
        error: 'Validation failed',
        details: result.error.flatten()
      });
    }
    req.body = result.data;
    next();
  };
}

// 使用示例
import { z } from 'zod';
const userSchema = z.object({
  username: z.string().min(3),
  email: z.string().email()
});

app.post('/users', createValidatorMiddleware(userSchema), (req, res) => {
  // req.body 现在有正确的类型提示
  const { username, email } = req.body;
  res.status(201).json({ username, email });
});

注意:这里使用了 Zod 进行运行时验证,确保类型安全不仅在编译时,也在运行时有效。

2.3 异步高阶中间件

对于需要异步操作的复杂中间件,我们可以构建高阶函数:

type AsyncMiddleware = (
  req: Request,
  res: Response,
  next: NextFunction
) => Promise<void>;

export function asyncWrapper(
  middleware: AsyncMiddleware
): RequestHandler {
  return (req, res, next) => {
    middleware(req, res, next).catch(next);
  };
}

// 使用示例
const authMiddleware = asyncWrapper(async (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) throw new Error('Missing token');
  
  const user = await verifyToken(token); // 假设的异步验证函数
  req.user = user;
  next();
});

app.get('/profile', authMiddleware, (req, res) => {
  res.json({ user: req.user });
});

这种模式解决了异步中间件的错误处理问题,同时保持了类型安全。

3. 类型化错误处理中间件

一个完整的错误处理方案应该包含类型化的错误类和对应的处理中间件:

// 定义自定义错误类型
export class AppError extends Error {
  constructor(
    public readonly message: string,
    public readonly statusCode: number = 500,
    public readonly details?: any
  ) {
    super(message);
  }
}

// 类型化错误处理中间件
export const errorHandler: ErrorRequestHandler = (
  err: unknown,
  req: Request,
  res: Response,
  next: NextFunction
) => {
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      error: err.message,
      ...(err.details && { details: err.details })
    });
  }

  if (err instanceof Error) {
    console.error('Unhandled error:', err);
    return res.status(500).json({ error: 'Internal server error' });
  }

  res.status(500).json({ error: 'Unknown error occurred' });
};

// 在路由中使用
app.post('/articles', async (req, res, next) => {
  try {
    const article = await createArticle(req.body);
    res.status(201).json(article);
  } catch (err) {
    if (err instanceof ValidationError) {
      next(new AppError('Invalid input', 400, err.errors));
    } else {
      next(err);
    }
  }
});

// 注册错误处理中间件(必须在路由之后)
app.use(errorHandler);

4. 完整类型化路由模块示例

下面是一个整合了所有技术的完整路由模块示例:

// src/routes/articles.ts
import { Router } from 'express';
import { z } from 'zod';
import { createValidatorMiddleware } from '../middlewares/validator';
import { asyncWrapper } from '../middlewares/asyncWrapper';
import { AppError } from '../errors';

const router = Router();

// 定义类型和验证模式
interface Article {
  id: string;
  title: string;
  content: string;
  authorId: string;
}

const articleInputSchema = z.object({
  title: z.string().min(5).max(100),
  content: z.string().min(10),
  authorId: z.string().uuid()
});

// 内存存储模拟数据库
const articles: Record<string, Article> = {};

// 创建文章路由
router.post(
  '/',
  createValidatorMiddleware(articleInputSchema),
  asyncWrapper(async (req, res) => {
    const newArticle: Article = {
      id: crypto.randomUUID(),
      ...req.body,
      createdAt: new Date()
    };
    articles[newArticle.id] = newArticle;
    res.status(201).json(newArticle);
  })
);

// 获取文章列表
router.get('/', asyncWrapper(async (req, res) => {
  const page = parseInt(req.query.page as string) || 1;
  const limit = parseInt(req.query.limit as string) || 10;
  
  if (page < 1 || limit < 1) {
    throw new AppError('Invalid pagination parameters', 400);
  }

  const result = Object.values(articles)
    .slice((page - 1) * limit, page * limit);
  
  res.json({
    data: result,
    meta: {
      page,
      limit,
      total: Object.keys(articles).length
    }
  });
}));

export default router;

5. 高级技巧:使用装饰器增强路由

TypeScript 5.5 改进了装饰器支持,我们可以利用它创建更优雅的 API:

// src/decorators/controller.ts
import { Router } from 'express';

export function Controller(prefix: string = '') {
  return (target: any) => {
    const router = Router();
    const instance = new target();
    
    // 收集所有路由方法
    for (const key of Object.getOwnPropertyNames(target.prototype)) {
      const routeHandler = instance[key];
      const path = Reflect.getMetadata('path', target.prototype, key);
      const method = Reflect.getMetadata('method', target.prototype, key);
      const middlewares = Reflect.getMetadata('middlewares', target.prototype, key) || [];
      
      if (path && method) {
        router[method](path, ...middlewares, routeHandler.bind(instance));
      }
    }
    
    return Router().use(prefix, router);
  };
}

export function Get(path: string, ...middlewares: any[]) {
  return (target: any, propertyKey: string) => {
    Reflect.defineMetadata('path', path, target, propertyKey);
    Reflect.defineMetadata('method', 'get', target, propertyKey);
    Reflect.defineMetadata('middlewares', middlewares, target, propertyKey);
  };
}

// 使用示例
@Controller('/products')
class ProductController {
  @Get('/')
  async listProducts(req: Request, res: Response) {
    res.json({ products: [] });
  }
}

// 在app.ts中注册
app.use(new ProductController());

这种模式虽然需要更多前期设置,但在大型项目中可以显著提高代码的组织性和可维护性。

6. 性能优化与生产环境建议

当项目规模增长时,类型检查可能会变慢。以下是一些优化技巧:

  1. 增量编译 :在 tsconfig.json 中启用 incremental: true
  2. 项目引用 :将大型项目拆分为多个子项目
  3. 类型导入 :使用 import type 减少运行时开销
  4. ESBuild 集成 :使用 esbuild-loader 加速开发服务器
// 生产环境错误处理增强
if (process.env.NODE_ENV === 'production') {
  app.use((err: any, req: Request, res: Response, next: NextFunction) => {
    console.error('Production error:', err);
    res.status(500).json({ error: 'Something went wrong' });
  });
}

在实际项目中,类型化的 Express.js 应用可以显著减少运行时错误,提高开发效率。通过合理组合基础类型、泛型和装饰器等高级特性,可以构建出既安全又灵活的后端架构。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐