Published on

Express 使用中间件 responseHandler 格式化响应数据

Authors
  • avatar
    Name
    Shelton Ma
    Twitter

常见约定

1. 统一响应结构

一个标准的 API 响应格式通常包含以下字段

{
  "success": true, // 状态码,通常使用 true 表示成功,false 表示失败.
  "message": "Success", // 请求的提示信息.
  "data": {}  // 请求的返回数据.
}

2. 在 TypeScript 中扩展 Response 类型

// src/typing.d.ts
import { ManagementClient, AuthenticationClient } from "authing-js-sdk";
import "express";

declare global {
  var authing: {
    managementClient: ManagementClient;
    authenticationClient: AuthenticationClient;
  };

  namespace Express {

    interface Response {
      exportRows?: any[];
      success: (params: { data?: any; message?: string }) => void;
      fail: (params: {
        httpStatus?: number;
        success?: boolean;
        message?: string;
        data?: any;
      }) => void;
    }
  }
}

3. 定义统一的 Response Middleware

// src/middlewares/response.middleware.ts
import { Request, Response, NextFunction } from "express";

interface SuccessResponse {
  data?: any;
  message?: string;
}

interface FailResponse {
  success?: boolean;
  message?: string;
  data?: any;
  httpStatus?: number;
}

// 统一响应格式封装
export function responseHandler(
  req: Request,
  res: Response,
  next: NextFunction
) {
  res.success = ({ data = null, message = "Success" }: SuccessResponse) => {
    res.json({ success: true, message, data });
  };

  res.fail = ({
    success = false,
    message = "Error",
    data = null,
    httpStatus = 400,
  }: FailResponse) => {
    res.status(httpStatus).json({ success, message, data });
  };

  next();
}

4. 控制器调用

router.post(
  "/",
  validateRequest(z.object({ body: TicketCreateSchema })),
  asyncHandler(createTicket)
);

5. 服务层调用

export const createTicket = async (
  req: TypedRequest<TicketCreateData>,
  res: Response
) => {
  res.success({ data: result, message: "Ticket created successfully" });
};

根据业务逻辑引入业务状态吗

1. 业务逻辑需要更细粒度的错误区分

HTTP 状态码(如 200, 400, 404)只能提供通用的状态,但无法区分具体的业务错误.例如:

场景HTTP 状态码**业务状态码 (code)message 解释
用户未登录4011001"未登录,请先登录"
用户权限不足4031002"无权限访问该资源"
账号已被冻结4031003"您的账号已被冻结,请联系客服"
余额不足4001004"账户余额不足,无法下单"

为什么不用 HTTP 状态码?

  • 401(未授权)无法区分未登录和Token 过期
  • 403(无权限)无法区分角色权限不足和账号被封禁
  • 400(请求错误)无法区分余额不足和参数错误

2. 使用code 可扩展、可国际化、可读性强

// 国际化
const errorMessages = {
  "1001": { en: "Not logged in", zh: "未登录" },
  "1002": { en: "Permission denied", zh: "无权限访问" },
  "1003": { en: "Account frozen", zh: "账号被冻结" }
};
showMessage(errorMessages[response.code][currentLang]);