InteractiveFrameworks

HTTP Module

Call another service from the cats API: HttpModule with a timeout, HttpService's observables turned into promises, and every way the upstream can fail translated into the right status for your own client.

What you'll learn

  • Import HttpModule with options and make requests through HttpService, converting its observables with firstValueFrom
  • Read an AxiosError: response status, timeout code, no response at all
  • Translate upstream failures into NotFoundException, GatewayTimeoutException and BadGatewayException instead of leaking a 500

So far every request has come to the cats API. Real services also make requests: to a payment provider, a geocoder, another team's API. The shelter does not keep breed facts; a breeds service does, and the cats API asks it. Nest's HTTP module wraps axios, the most used HTTP client in the Node world, and hands it over as an injectable HttpService, which returns Observables. This lesson makes the calls, and spends most of its time on the part that separates a toy from a service: what to do when the other side is slow, wrong or gone.

HttpModule and HttpService

@nestjs/axios provides the module; axios does the work:

@Module({
  imports: [HttpModule],
  providers: [CatsService],
})
export class CatsModule {}

Inject HttpService and call it like axios, with the same methods, get, post, put, delete, patch, and the same config object:

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

  findAll(): Observable<AxiosResponse<Cat[]>> {
    return this.httpService.get('http://localhost:3000/cats');
  }
}

The difference from raw axios is the return type: an Observable of the response rather than a Promise, so rxjs operators apply. Most services want a promise in the end, and firstValueFrom converts:

async findAll(): Promise<Cat[]> {
  const { data } = await firstValueFrom(
    this.httpService.get<Cat[]>('http://localhost:3000/cats').pipe(
      catchError((error: AxiosError) => {
        this.logger.error(error.response.data);
        throw 'An error happened!';
      }),
    ),
  );
  return data;
}

AxiosResponse carries data, status, headers; the generic on get<Cat[]> types data. When you want axios itself, for an interceptor or a feature the wrapper does not expose, this.httpService.axiosRef is the underlying instance.

Configuration

HttpModule.register({ timeout: 5000, maxRedirects: 5 }) passes axios defaults to every request the module makes; baseURL and headers belong there too. registerAsync({ useFactory, inject }) builds them from configuration, as every module in this chapter does, and useClass/useExisting take a factory class implementing HttpModuleOptionsFactory.

When the other side fails

An outbound request can fail in three ways, and each deserves a different answer to your own client. The upstream answered with an error: error.response exists, with status and data; a 404 there usually means your client asked for something that does not exist, so answer 404 too. The upstream did not answer in time: with timeout set, axios aborts and reports error.code === 'ECONNABORTED' (ETIMEDOUT in some paths), and the honest answer is 504 Gateway Timeout. The upstream could not be reached or broke: no response, or a 5xx; answer 502 Bad Gateway. Nest has an exception class for each, and catchError inside the pipe is where you translate. What you must not do is let an AxiosError escape unmapped: it becomes a 500 with Internal server error, and the client learns nothing.

A timeout is not optional. Without one, a hung upstream hangs your handler, and enough of those hang your server. Set it in register(), once.

In this runtime

The breeds service, https://api.catbreeds.example, is simulated by fixtures declared for this lesson: GET /breeds and GET /breeds/siamese answer, GET /breeds/sphynx is a 404, and GET /breeds/slow takes longer than your timeout allows. The code you write is exactly the code you would ship against the real thing.

Your task

The cats API asks the breeds service about breeds.

  1. CatsModule imports the HTTP module with a two-second timeout.
  2. BreedsService.findAll fetches ${BREEDS_API}/breeds; findOne(slug) fetches ${BREEDS_API}/breeds/${slug}. Both convert the observable to a promise and return data.
  3. Translate failures: a 404 from the upstream is NotFoundException No such breed: <slug>; a timeout is GatewayTimeoutException The breeds service did not answer in time; anything else is BadGatewayException The breeds service failed.

Run after step 2 and request GET /breeds/sphynx: an unmapped AxiosError is a 500, and the console shows what axios knew that your client was not told.

When it fails

  • Nest can't resolve dependencies of the BreedsService (?), argument HttpService: HttpModule is not imported by the module that provides the service.
  • 500 Internal server error for a breed that does not exist: the AxiosError escaped; catch it and look at error.response?.status.
  • GET /breeds/slow answers after a long wait, or as a 502: no timeout in register(), or the timeout code is not recognised; check error.code.
  • The handler returns an object with _subscribe in it: the observable was returned as data; convert with firstValueFrom.

Remember

  • HttpModule.register({ timeout }) in the module; HttpService in the constructor; get<T>(url) returns Observable<AxiosResponse<T>>.
  • firstValueFrom(observable.pipe(catchError(...))) gives a promise, and the pipe is where errors are translated.
  • Map upstream failures deliberately: 404NotFoundException, timeout → GatewayTimeoutException, the rest → BadGatewayException.
  • Always set a timeout on outbound requests.
Stuck? Show a hint

cats.module.ts: imports: [..., HttpModule.register({ timeout: 2000 })] from '@nestjs/axios'. Service: constructor(private readonly http: HttpService) {}; const { data } = await firstValueFrom(this.http.get<Breed>(url).pipe(catchError((error: AxiosError) => { ... throw ...; }))); inside catchError: error.response?.status === 404 → NotFoundException; error.code === 'ECONNABORTED' → GatewayTimeoutException; otherwise BadGatewayException.