进阶实战:RESTful API封装与类型安全

本实战项目旨在通过TypeScript与Node.js构建一个类型安全的RESTful API客户端,深入掌握TypeScript在后端开发中的核心应用,特别是类型系统、泛型工具与装饰器模式的实践。通过封装统一的API调用层,实现请求/响应类型约束、错误处理与日志记录的标准化,为大型应用提供可靠的接口交互方案。

需求分析与技术栈准备

在开始实现前,需明确API客户端的核心需求,确保覆盖RESTful API交互的关键场景:

  • 请求方法支持:需兼容GET、POST、PUT、DELETE四种核心HTTP方法,满足资源的CRUD操作;
  • 类型安全保障:通过TypeScript类型系统约束请求参数与响应数据结构,在编译阶段捕获类型错误;
  • 错误统一处理:对网络错误、状态码异常、业务错误等场景进行集中拦截与格式化;
  • 请求日志记录:通过装饰器模式实现请求详情(URL、方法、耗时等)的自动记录,便于调试与监控。

技术栈选择需围绕类型安全与开发效率展开:

  • 运行环境:Node.js(v16+)提供基础运行能力;
  • HTTP客户端:axios库处理请求发送与响应拦截,其拦截器机制便于扩展;
  • TypeScript配置:关键配置项需确保类型解析准确性,推荐设置module: ESNext(支持现代模块系统)与moduleResolution: bundler(优化模块查找逻辑),同时启用strict: true增强类型检查严格性。

核心实现步骤

1. 类型系统设计

类型定义是确保API类型安全的基础,需先明确核心类型与接口:

typescript
复制代码
// 基础配置接口:定义API客户端的全局设置
interface ApiConfig {
  baseURL: string;  // API基础路径,如"https://api.example.com"
  timeout: number;  // 请求超时时间(毫秒),如5000
}

// HTTP方法类型:限制仅支持RESTful核心方法,避免非法请求
type HttpMethod = 'get' | 'post' | 'put' | 'delete';

// 响应数据通用结构:假设后端返回格式统一为{ code: number; data: T; message?: string }
interface ApiResponse<T> {
  code: number;       // 业务状态码(如200表示成功,400表示参数错误)
  data: T;            // 实际响应数据,通过泛型T指定具体类型
  message?: string;   // 可选消息描述
}

类型安全关键:通过ApiResponse<T>泛型接口统一响应格式,确保业务数据data的类型与调用方声明一致。例如,用户信息接口可声明为ApiResponse<User>,TypeScript将自动校验返回数据是否符合User接口结构。

2. 基础API客户端封装

基于axios封装核心ApiClient类,实现请求发送的底层逻辑,并通过泛型方法确保响应类型安全:

typescript
复制代码
import axios, { AxiosRequestConfig, AxiosError } from 'axios';

class ApiClient {
  private instance;  // axios实例

  constructor(config: ApiConfig) {
    // 初始化axios实例,应用基础配置
    this.instance = axios.create({
      baseURL: config.baseURL,
      timeout: config.timeout,
      headers: { 'Content-Type': 'application/json' }
    });
  }

  /**
   * 泛型请求方法:核心类型安全保障
   * @param method HTTP请求方法
   * @param url 请求路径(相对于baseURL)
   * @param data 请求数据(POST/PUT时使用)
   * @returns Promise<T> 响应数据(自动解析ApiResponse<T>中的data字段)
   */
  async request<T>(
    method: HttpMethod,
    url: string,
    data?: any
  ): Promise<T> {
    try {
      // 构建axios请求配置
      const config: AxiosRequestConfig = { method, url };
      
      // 根据方法类型设置数据字段(GET用params,其他用data)
      if (method === 'get') {
        config.params = data;
      } else {
        config.data = data;
      }

      // 发送请求并获取响应,通过泛型约束响应类型
      const response = await this.instance.request<ApiResponse<T>>(config);
      
      // 仅返回业务数据部分(剥离code和message)
      return response.data.data;
    } catch (error) {
      // 错误统一抛出,由装饰器处理
      throw error;
    }
  }
}

泛型方法解析request<T>通过泛型参数T指定响应数据类型,结合ApiResponse<T>接口,实现三层类型保障:

  1. 请求方法约束method参数限制为HttpMethod,避免拼写错误(如"get"误写为"GET");
  2. 响应结构约束instance.request<ApiResponse<T>>确保后端返回符合统一响应格式;
  3. 数据类型提取:返回response.data.data时自动推断为T类型,调用方无需手动类型转换。
3. 装饰器增强功能

利用TypeScript装饰器(Decorator)模式,在不修改ApiClient核心逻辑的前提下,添加请求日志与错误处理能力:

typescript
复制代码
import { RequestMethod } from 'axios';

/**
 * 请求日志装饰器:记录请求详情与耗时
 */
function logRequest(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = async function(...args: any[]) {
    const [method, url] = args;  // 提取方法和URL参数
    const startTime = Date.now();
    
    console.log(`[API Request] ${method.toUpperCase()} ${url} - Start`);
    
    try {
      const result = await originalMethod.apply(this, args);
      const duration = Date.now() - startTime;
      console.log(`[API Request] ${method.toUpperCase()} ${url} - Success (${duration}ms)`);
      return result;
    } catch (error) {
      const duration = Date.now() - startTime;
      console.error(`[API Request] ${method.toUpperCase()} ${url} - Failed (${duration}ms)`);
      throw error;
    }
  };
  
  return descriptor;
}

/**
 * 错误处理装饰器:统一格式化错误信息
 */
function handleError(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = async function(...args: any[]) {
    try {
      return await originalMethod.apply(this, args);
    } catch (error) {
      // 区分axios错误与其他错误
      if (error instanceof AxiosError) {
        const { response } = error;
        // 格式化错误信息:包含状态码、路径与响应内容
        const errorMsg = `API Error: ${response?.status || 'Network Error'} - ${response?.data?.message || error.message}`;
        throw new Error(errorMsg);
      } else {
        // 非axios错误直接抛出
        throw new Error(`Request Error: ${error.message}`);
      }
    }
  };
  
  return descriptor;
}

// 在ApiClient的request方法上应用装饰器
class ApiClient {
  // ... 其他代码不变 ...
  
  @logRequest
  @handleError
  async request<T>(method: HttpMethod, url: string, data?: any): Promise<T> {
    // ... 原有实现 ...
  }
}

装饰器拦截逻辑:装饰器通过重写request方法实现功能增强,执行流程为:

  1. 日志装饰器:记录请求开始时间→执行原方法→记录成功/失败日志与耗时;
  2. 错误装饰器:捕获原方法抛出的错误→区分网络错误/业务错误→格式化错误信息后重新抛出;
  3. 执行顺序:装饰器应用顺序为从下到上(先@handleError@logRequest),但实际拦截顺序为日志先执行(因装饰器工厂函数执行顺序相反)。
4. 业务API模块实现

基于ApiClient扩展业务接口,以用户模块(userApi.ts)为例,定义具体请求/响应类型并实现接口方法:

typescript
复制代码
// 用户相关类型定义
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: string;
}

interface CreateUserRequest {
  name: string;
  email: string;
  password: string;
}

// 用户API客户端,继承基础ApiClient
class UserApi extends ApiClient {
  constructor(config: ApiConfig) {
    super(config);
  }

  /**
   * 获取用户列表
   * @returns 用户数组(类型自动推断为User[])
   */
  getUsers() {
    return this.request<User[]>('get', '/users');
  }

  /**
   * 创建新用户
   * @param data 创建用户请求数据(类型约束为CreateUserRequest)
   * @returns 创建成功的用户信息(类型为User)
   */
  createUser(data: CreateUserRequest) {
    return this.request<User>('post', '/users', data);
  }

  /**
   * 更新用户信息
   * @param id 用户ID
   * @param data 更新数据(部分User字段)
   * @returns 更新后的用户信息
   */
  updateUser(id: number, data: Partial<User>) {
    return this.request<User>('put', `/users/${id}`, data);
  }

  /**
   * 删除用户
   * @param id 用户ID
   * @returns 空响应(类型为void)
   */
  deleteUser(id: number) {
    return this.request<void>('delete', `/users/${id}`);
  }
}

业务类型安全:通过为每个接口方法指定具体泛型参数(如getUsers()User[]),实现:

  • 请求参数校验createUser(data)data必须符合CreateUserRequest结构,缺失nameemail将触发编译错误;
  • 返回类型提示:调用userApi.getUsers()时,IDE自动提示返回数组的每个元素具有idnameUser接口字段;
  • 重构安全性:若后端修改User接口(如新增status字段),仅需更新User类型定义,所有调用处将自动获得类型提示。

测试与优化

1. 类型安全测试(Jest)

通过Jest结合TypeScript类型断言,验证接口调用的类型约束:

typescript
复制代码
import { UserApi } from './userApi';

// 模拟API配置
const apiConfig = { baseURL: 'https://api.example.com', timeout: 5000 };
const userApi = new UserApi(apiConfig);

describe('UserApi类型安全测试', () => {
  it('getUsers应返回User[]类型', async () => {
    // 模拟axios响应
    jest.spyOn(userApi['instance'], 'request').mockResolvedValue({
      data: { code: 200, data: [{ id: 1, name: 'Test', email: 'test@example.com', createdAt: '2023-01-01' }] }
    });

    const users = await userApi.getUsers();
    // 类型断言测试:验证返回值符合User[]结构
    expect(users[0]).toHaveProperty('id');
    expect(users[0]).toHaveProperty('name');
  });

  it('createUser应校验请求参数类型', () => {
    // @ts-expect-error 故意传入错误参数,验证TypeScript编译错误
    userApi.createUser({ name: 'Test' });  // 缺失email和password,应报错
  });
});
2. 性能优化方向
  • 请求缓存:基于method+url+data生成唯一键,使用Map缓存请求结果,避免重复请求:

    typescript
    复制代码
    private cache = new Map<string, any>();
    
    // 在request方法中添加缓存逻辑
    const cacheKey = `${method}-${url}-${JSON.stringify(data)}`;
    if (method === 'get' && this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey);
    }
    // 请求成功后存入缓存
    this.cache.set(cacheKey, response.data.data);
  • 重试机制:对网络错误或5xx状态码自动重试,通过装饰器实现:

    typescript
    复制代码
    function retry(maxRetries = 3) {
      return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
        const originalMethod = descriptor.value;
        descriptor.value = async function(...args: any[]) {
          let retries = 0;
          while (retries < maxRetries) {
            try {
              return await originalMethod.apply(this, args);
            } catch (error) {
              retries++;
              if (retries >= maxRetries) throw error;
              await new Promise(res => setTimeout(res, 1000 * retries)); // 指数退避
            }
          }
        };
      };
    }

扩展练习:请求取消功能

利用浏览器/Node.js原生的AbortController实现请求取消,允许调用方终止未完成的请求:

typescript
复制代码
// 定义取消令牌类型
type CancelToken = AbortController;

// 增强request方法支持取消令牌
async request<T>(
  method: HttpMethod,
  url: string,
  data?: any,
  cancelToken?: CancelToken
): Promise<T> {
  try {
    const config: AxiosRequestConfig = { 
      method, 
      url,
      signal: cancelToken?.signal  // 传递AbortSignal
    };
    // ... 其余逻辑不变 ...
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Request aborted');
    }
    throw error;
  }
}

// 使用示例
const controller = new AbortController();
userApi.getUsers(controller.signal);
// 取消请求(如组件卸载时)
controller.abort();

通过上述实现,我们构建了一个兼具类型安全、可扩展性与可维护性的RESTful API客户端,充分发挥了TypeScript在类型约束、泛型工具与装饰器模式上的优势,为企业级应用的接口交互提供了标准化解决方案。