NestJS HTTP客户端

学习目标

  • 掌握NestJS HTTP模块的使用方法
  • 理解HTTP请求的配置选项
  • 学习如何处理HTTP响应
  • 了解HTTP拦截器的使用场景和实现方法
  • 掌握HTTP客户端的错误处理策略

核心知识点

1. HTTP模块简介

NestJS的HTTP客户端基于Axios库,通过@nestjs/axios包提供。它允许我们在NestJS应用中发送HTTP请求到外部API或服务。HTTP模块提供了以下功能:

  • 发送GET、POST、PUT、DELETE等HTTP请求
  • 配置请求头、参数、超时等选项
  • 处理HTTP响应和错误
  • 使用拦截器修改请求和响应
  • 支持请求取消和重试

2. 安装和配置

首先,我们需要安装HTTP模块:

npm install --save @nestjs/axios axios

然后,在需要使用HTTP客户端的模块中导入HTTP模块:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    HttpModule.register({
      timeout: 5000,
      maxRedirects: 5,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

3. 基本使用

3.1 在服务中使用

// src/app.service.ts
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { lastValueFrom } from 'rxjs';

@Injectable()
export class AppService {
  constructor(private readonly httpService: HttpService) {}

  async getHello(): Promise<string> {
    const response = await lastValueFrom(
      this.httpService.get('https://api.example.com/hello'),
    );
    return response.data;
  }
}

3.2 发送不同类型的请求

// src/app.service.ts
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { lastValueFrom } from 'rxjs';

@Injectable()
export class AppService {
  constructor(private readonly httpService: HttpService) {}

  // 发送GET请求
  async getUsers(): Promise<any> {
    const response = await lastValueFrom(
      this.httpService.get('https://api.example.com/users', {
        params: { page: 1, limit: 10 },
        headers: {
          'Authorization': 'Bearer token123',
        },
      }),
    );
    return response.data;
  }

  // 发送POST请求
  async createUser(user: any): Promise<any> {
    const response = await lastValueFrom(
      this.httpService.post('https://api.example.com/users', user, {
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer token123',
        },
      }),
    );
    return response.data;
  }

  // 发送PUT请求
  async updateUser(id: string, user: any): Promise<any> {
    const response = await lastValueFrom(
      this.httpService.put(`https://api.example.com/users/${id}`, user),
    );
    return response.data;
  }

  // 发送DELETE请求
  async deleteUser(id: string): Promise<any> {
    const response = await lastValueFrom(
      this.httpService.delete(`https://api.example.com/users/${id}`),
    );
    return response.data;
  }
}

4. 请求配置

HTTP模块支持以下配置选项:

  • baseURL: 请求的基础URL
  • timeout: 请求超时时间(毫秒)
  • maxRedirects: 最大重定向次数
  • headers: 默认请求头
  • paramsSerializer: 参数序列化方法
  • withCredentials: 是否携带凭证
  • auth: 基本认证信息
  • responseType: 响应类型
  • xsrfCookieName: XSRF cookie名称
  • xsrfHeaderName: XSRF头名称

5. 响应处理

HTTP响应包含以下信息:

  • data: 响应数据
  • status: HTTP状态码
  • statusText: HTTP状态文本
  • headers: 响应头
  • config: 请求配置
// src/app.service.ts
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { lastValueFrom } from 'rxjs';

@Injectable()
export class AppService {
  constructor(private readonly httpService: HttpService) {}

  async getUsers(): Promise<any> {
    const response = await lastValueFrom(
      this.httpService.get('https://api.example.com/users'),
    );
    
    console.log('Status:', response.status);
    console.log('Status Text:', response.statusText);
    console.log('Headers:', response.headers);
    console.log('Data:', response.data);
    
    return response.data;
  }
}

6. HTTP拦截器

HTTP拦截器允许我们在发送请求前或接收响应后修改请求或响应。我们可以使用拦截器来添加认证令牌、处理错误、添加日志等。

6.1 创建请求拦截器

// src/common/interceptors/request.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { HttpRequest } from '@nestjs/common/http';

@Injectable()
export class RequestInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest<HttpRequest>();
    
    // 添加认证令牌
    request.headers['Authorization'] = 'Bearer token123';
    
    // 添加请求时间戳
    request.headers['X-Request-Time'] = new Date().toISOString();
    
    console.log('Request Interceptor:', request.url);
    
    return next.handle();
  }
}

6.2 创建响应拦截器

// src/common/interceptors/response.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    
    return next.handle().pipe(
      map((response) => {
        const elapsed = Date.now() - now;
        console.log(`Response Interceptor: ${elapsed}ms`);
        
        // 可以在这里修改响应数据
        return response;
      }),
    );
  }
}

6.3 在模块中使用拦截器

// src/app.module.ts
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { RequestInterceptor } from './common/interceptors/request.interceptor';
import { ResponseInterceptor } from './common/interceptors/response.interceptor';

@Module({
  imports: [
    HttpModule.register({
      timeout: 5000,
      maxRedirects: 5,
    }),
  ],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: 'APP_INTERCEPTOR',
      useClass: RequestInterceptor,
    },
    {
      provide: 'APP_INTERCEPTOR',
      useClass: ResponseInterceptor,
    },
  ],
})
export class AppModule {}

7. 错误处理

HTTP客户端可能会遇到以下类型的错误:

  • 网络错误:如连接超时、网络不可用等
  • HTTP错误:如404 Not Found、500 Internal Server Error等
  • 响应解析错误:如响应数据格式不正确等
// src/app.service.ts
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { lastValueFrom, catchError } from 'rxjs';
import { AxiosError } from 'axios';

@Injectable()
export class AppService {
  constructor(private readonly httpService: HttpService) {}

  async getUsers(): Promise<any> {
    try {
      const response = await lastValueFrom(
        this.httpService.get('https://api.example.com/users').pipe(
          catchError((error: AxiosError) => {
            console.error('HTTP Error:', error.message);
            
            if (error.response) {
              // 服务器返回错误状态码
              console.error('Response Data:', error.response.data);
              console.error('Response Status:', error.response.status);
              console.error('Response Headers:', error.response.headers);
              
              throw new HttpException(
                error.response.data || 'Server error',
                error.response.status,
              );
            } else if (error.request) {
              // 请求已发送但没有收到响应
              console.error('Request:', error.request);
              
              throw new HttpException(
                'No response received from server',
                HttpStatus.GATEWAY_TIMEOUT,
              );
            } else {
              // 请求配置错误
              console.error('Request Error:', error.message);
              
              throw new HttpException(
                'Request configuration error',
                HttpStatus.BAD_REQUEST,
              );
            }
          }),
        ),
      );
      
      return response.data;
    } catch (error) {
      console.error('Error:', error);
      throw error;
    }
  }
}

实用案例分析

案例1:调用外部API

需求分析

我们需要实现一个服务,调用外部天气API获取天气信息,并将结果返回给客户端。

实现方案

  1. 创建天气服务
// src/weather/weather.service.ts
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { lastValueFrom, catchError } from 'rxjs';
import { AxiosError } from 'axios';

@Injectable()
export class WeatherService {
  constructor(private readonly httpService: HttpService) {}

  async getWeather(city: string): Promise<any> {
    const apiKey = process.env.WEATHER_API_KEY;
    const url = `https://api.weatherapi.com/v1/current.json`;
    
    try {
      const response = await lastValueFrom(
        this.httpService.get(url, {
          params: {
            key: apiKey,
            q: city,
          },
        }).pipe(
          catchError((error: AxiosError) => {
            console.error('Weather API Error:', error.message);
            
            if (error.response) {
              throw new HttpException(
                error.response.data || 'Weather API error',
                error.response.status,
              );
            } else {
              throw new HttpException(
                'Failed to connect to weather API',
                HttpStatus.SERVICE_UNAVAILABLE,
              );
            }
          }),
        ),
      );
      
      return response.data;
    } catch (error) {
      throw error;
    }
  }
}
  1. 创建天气控制器
// src/weather/weather.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { WeatherService } from './weather.service';

@Controller('weather')
export class WeatherController {
  constructor(private readonly weatherService: WeatherService) {}

  @Get()
async getWeather(@Query('city') city: string) {
    return this.weatherService.getWeather(city);
  }
}
  1. 创建天气模块
// src/weather/weather.module.ts
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { WeatherController } from './weather.controller';
import { WeatherService } from './weather.service';

@Module({
  imports: [
    HttpModule.register({
      timeout: 10000,
      maxRedirects: 5,
    }),
  ],
  controllers: [WeatherController],
  providers: [WeatherService],
  exports: [WeatherService],
})
export class WeatherModule {}

案例2:实现API网关

需求分析

我们需要实现一个API网关,作为前端和后端微服务之间的中间层,转发请求到相应的微服务,并处理响应和错误。

实现方案

  1. 创建API网关服务
// src/gateway/gateway.service.ts
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { lastValueFrom, catchError } from 'rxjs';
import { AxiosError } from 'axios';

@Injectable()
export class GatewayService {
  constructor(private readonly httpService: HttpService) {}

  async forwardRequest(method: string, service: string, endpoint: string, data?: any, params?: any): Promise<any> {
    // 服务地址映射
    const serviceUrls = {
      users: 'http://users-service:3000',
      products: 'http://products-service:3000',
      orders: 'http://orders-service:3000',
    };
    
    const baseUrl = serviceUrls[service as keyof typeof serviceUrls];
    if (!baseUrl) {
      throw new HttpException('Service not found', HttpStatus.NOT_FOUND);
    }
    
    const url = `${baseUrl}${endpoint}`;
    const config = {
      params,
      headers: {
        'Content-Type': 'application/json',
      },
    };
    
    try {
      let response;
      
      switch (method.toUpperCase()) {
        case 'GET':
          response = await lastValueFrom(
            this.httpService.get(url, config).pipe(
              catchError(this.handleError),
            ),
          );
          break;
        case 'POST':
          response = await lastValueFrom(
            this.httpService.post(url, data, config).pipe(
              catchError(this.handleError),
            ),
          );
          break;
        case 'PUT':
          response = await lastValueFrom(
            this.httpService.put(url, data, config).pipe(
              catchError(this.handleError),
            ),
          );
          break;
        case 'DELETE':
          response = await lastValueFrom(
            this.httpService.delete(url, config).pipe(
              catchError(this.handleError),
            ),
          );
          break;
        default:
          throw new HttpException('Invalid HTTP method', HttpStatus.BAD_REQUEST);
      }
      
      return response.data;
    } catch (error) {
      throw error;
    }
  }

  private handleError = (error: AxiosError) => {
    console.error('Gateway Error:', error.message);
    
    if (error.response) {
      throw new HttpException(
        error.response.data || 'Service error',
        error.response.status,
      );
    } else if (error.request) {
      throw new HttpException(
        'Service unavailable',
        HttpStatus.SERVICE_UNAVAILABLE,
      );
    } else {
      throw new HttpException(
        'Request error',
        HttpStatus.BAD_REQUEST,
      );
    }
  };
}
  1. 创建API网关控制器
// src/gateway/gateway.controller.ts
import { Controller, All, Req, Res, Body, Query } from '@nestjs/common';
import { Request, Response } from 'express';
import { GatewayService } from './gateway.service';

@Controller('api')
export class GatewayController {
  constructor(private readonly gatewayService: GatewayService) {}

  @All('*')
async handleRequest(@Req() req: Request, @Res() res: Response, @Body() body: any, @Query() query: any) {
    try {
      // 从URL中提取服务名和端点
      // 格式: /api/{service}/{endpoint}
      const parts = req.url.split('/').filter(Boolean);
      if (parts.length < 2) {
        return res.status(400).json({ message: 'Invalid URL format' });
      }
      
      const service = parts[1];
      const endpoint = '/' + parts.slice(2).join('/');
      
      const result = await this.gatewayService.forwardRequest(
        req.method,
        service,
        endpoint,
        body,
        query,
      );
      
      return res.json(result);
    } catch (error) {
      const statusCode = error.status || 500;
      const message = error.message || 'Internal server error';
      return res.status(statusCode).json({ message });
    }
  }
}
  1. 创建API网关模块
// src/gateway/gateway.module.ts
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { GatewayController } from './gateway.controller';
import { GatewayService } from './gateway.service';

@Module({
  imports: [
    HttpModule.register({
      timeout: 30000,
      maxRedirects: 5,
    }),
  ],
  controllers: [GatewayController],
  providers: [GatewayService],
  exports: [GatewayService],
})
export class GatewayModule {}

常见问题与解决方案

1. 请求超时

可能原因

  • 网络延迟
  • 外部服务响应慢
  • 超时设置过短

解决方案

  • 增加超时时间
  • 实现请求重试机制
  • 优化外部服务性能

2. 认证失败

可能原因

  • 认证令牌过期
  • 认证令牌格式错误
  • 权限不足

解决方案

  • 实现令牌刷新机制
  • 确保令牌格式正确
  • 检查用户权限

3. CORS错误

可能原因

  • 跨域请求被拒绝
  • CORS配置不当

解决方案

  • 在外部服务中配置正确的CORS策略
  • 使用代理服务器

4. 响应数据格式错误

可能原因

  • 外部服务返回格式与预期不符
  • 响应数据解析错误

解决方案

  • 增加数据验证
  • 实现容错机制
  • 与外部服务提供者协调数据格式

最佳实践

  1. 服务封装:将外部API调用封装到专门的服务中,提高代码可维护性
  2. 配置管理:将API地址、超时时间等配置放到配置文件中
  3. 错误处理:实现统一的错误处理机制,提高代码健壮性
  4. 日志记录:为HTTP请求和响应添加详细的日志记录
  5. 缓存策略:对频繁访问的数据使用缓存,减少HTTP请求
  6. 请求限流:实现请求限流,避免过度调用外部API
  7. 监控告警:监控HTTP请求的成功率和响应时间,及时发现问题

代码优化建议

  1. 使用装饰器:创建自定义装饰器简化HTTP请求的发送
  2. 实现重试机制:为失败的请求添加自动重试功能
  3. 使用拦截器:利用拦截器统一处理认证、日志等横切关注点
  4. 类型定义:为请求和响应数据添加TypeScript类型定义
  5. 测试覆盖:为HTTP客户端代码编写单元测试和集成测试

总结

NestJS的HTTP客户端模块提供了一种简洁、高效的方式来发送HTTP请求和处理响应。通过本文的学习,你应该已经掌握了:

  • 如何安装和配置HTTP模块
  • 如何发送不同类型的HTTP请求
  • 如何配置请求选项和处理响应
  • 如何使用HTTP拦截器
  • 如何处理HTTP错误
  • 如何实现外部API调用和API网关

HTTP客户端是现代应用程序的重要组成部分,它允许我们与外部服务进行通信,构建更加复杂和功能丰富的应用。合理使用NestJS的HTTP客户端功能,可以让你的应用程序更好地与外部世界集成。

互动问答

  1. 以下哪个是NestJS HTTP模块的正确安装命令?
    A. npm install --save @nestjs/http
    B. npm install --save @nestjs/axios
    C. npm install --save http
    D. npm install --save axios

  2. 如何在NestJS中发送带有参数的GET请求?

  3. 如何使用HTTP拦截器添加认证令牌?

  4. 如何处理HTTP客户端的网络错误?

  5. 如何实现API网关来转发请求到微服务?

« 上一篇 NestJS国际化 下一篇 » NestJS健康检查