Downloading files with NestJS

Setup type-safe endpoints for downloading files in your NestJS application.

Authors
Marc Stammerjohann Marc Stammerjohann
Published at

You got file upload figured out in your NestJS application? And now you need to download files from your REST API? 🤓

Glad you're here now, but make sure you prepare your Nest app with Swagger first.

Want to jump directly to the code? Here is the repository.

File download

Downloading a file with Nest depends on how you retrieve it from your file storage:

  • as Buffer use response.send(fileBuffer)
  • as Stream use fileStream.pipe(response)

This will get the job done easily but you'll loose access to the response during response interceptors. See the LoggingInterceptor as an example as interceptor.

As an alternative, Nest provides StreamableFile, which solves the response interceptor problem, and supports both Buffer and Stream in one swoop. 🦾

import {
  Controller,
  Get,
  Res,
  StreamableFile,
  UseInterceptors,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { DownloadService } from './download.service';
import { Response } from 'express';
import { LoggingInterceptor } from 'src/logging.interceptor';

@UseInterceptors(LoggingInterceptor)
@Controller('download')
@ApiTags('download')
export class DownloadController {
  constructor(private readonly downloadService: DownloadService) {}

  @Get('buffer')
  buffer(@Res() response: Response) {
    const file = this.downloadService.imageBuffer();
    response.send(file);
  }

  @Get('stream')
  stream(@Res() response: Response) {
    const file = this.downloadService.imageStream();
    file.pipe(response);
  }

  @Get('streamable')
  streamable(@Res({ passthrough: true }) response: Response) {
    const file = this.downloadService.fileStream();
    // or
    // const file = this.downloadService.fileBuffer();
    return new StreamableFile(file); // 👈 supports Buffer and Stream
  }
}
import { Injectable } from '@nestjs/common';
import { createReadStream, readFileSync } from 'fs';
import { join } from 'path';

/**
 * This service would probably download files from a file storage
 * like S3, minio etc.
 */
@Injectable()
export class DownloadService {
  constructor() {
    // create connection to your file storage
  }

  imageBuffer() {
    return readFileSync(join(process.cwd(), 'notiz.png'));
  }

  imageStream() {
    return createReadStream(join(process.cwd(), 'notiz.png'));
  }

  fileBuffer() {
    return readFileSync(join(process.cwd(), 'package.json'));
  }

  fileStream() {
    return createReadStream(join(process.cwd(), 'package.json'));
  }
}
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');

    const now = Date.now();
    return next.handle().pipe(
      tap(() => console.log(`After... ${Date.now() - now}ms`)),
      tap((response) => console.log(response)), // 👈 response is defined only when StreamableFile is used
    );
  }
}

Custom headers

Express Response allows you to modify the response headers based on your needs. This is possible for all three options described above. Very important to note here is to configure the response @Res({ passthrough: true }) when using StreamableFile, otherwise the response won't end.

Here are some examples how to customize the response headers for your endpoints or use response.setHeaders(...) for complete custom headers like caching.

Change the content type

By default application/octet-stream is set as the Content-Type. If you know you are returning an image or a document from your endpoint change the content type.

@Get('buffer')
buffer(@Res() response: Response) {
  const file = this.downloadService.imageBuffer();
  response.contentType('png');
  // response.contentType('image/png');
  // response.contentType('image/*');
  // response.contentType('application/pdf');
  response.send(file);
}

Preview or download file

Use the header Content-Disposition to inform if the file should be displayed inline, the default, or downloaded as an attachment. Use response.attachment() to indicate the file as download and optionally pass a filename.

@Get('buffer')
buffer(@Res() response: Response) {
  const file = this.downloadService.imageBuffer();
  response.contentType('image/png');
  response.attachment();
  // provide a filename
  // response.attachment('notiz.png');
  response.send(file);
}

Swagger Types

Until now, you covered the response part of the Rest API. How about Swagger? Does Swagger already know that the endpoint response is a file? Let's check it out at http://localhost:3000/api.

Download file without Swagger types

Swagger is not aware about the file response and not even your custom content type. There is a way of telling Swagger about it. Can you spot the two decorators for this job? 🧐

The decorators for the job are @ApiResponse and @ApiProduces. 🤝

@ApiResponse is responsible for changing the response schema to a binary format. Similar to what you had to do for the type-safe file upload.

import { ApiResponse } from '@nestjs/swagger';

@ApiResponse({
  schema: {
    type: 'string',
    format: 'binary',
  },
  status: HttpStatus.OK,
})
@Get('buffer')
buffer(@Res() response: Response) {
  const file = this.downloadService.imageBuffer();
  response.contentType('image/png');
  response.send(file);
}

You can easily replace the decorator @ApiResponse with predefined status codes like @ApiOkResponse, @ApiCreatedResponse and skip the status code option.

Next, use @ApiProduces to inform Swagger about the response content type. It supports multiple content types and also wildcards (these things */*, image/*).

import { ApiOkResponse, ApiProduces } from '@nestjs/swagger';

@ApiOkResponse({
  schema: {
    type: 'string',
    format: 'binary',
  },
})
@ApiProduces('image/png')
@Get('buffer')
buffer(@Res() response: Response) {
  const file = this.downloadService.imageBuffer();
  response.contentType('image/png');
  response.send(file);
}

Your Swagger types should look as awesome as this. 👇

Download file with Swagger types

Great, are you done yet? You are good to go, but there is one optimization you can do for simplifying the applied decorators. Why not create a custom @ApiFileResponse decorator to finish it up.

File response decorator

Add a new file api-file-response.decorator.ts and export a function called ApiFileResponse like the decorator. You'll create a composition of @ApiOkResponse and @ApiProduces.

import { applyDecorators } from '@nestjs/common';
import { ApiOkResponse, ApiProduces } from '@nestjs/swagger';

export function ApiFileResponse(...mimeTypes: string[]) {
  return applyDecorators(
    ApiOkResponse({
      schema: {
        type: 'string',
        format: 'binary',
      },
    }),
    ApiProduces(...mimeTypes),
  );
}

That's looking good and you can still pass any content type (mimeTypes) as you wish. And it is as simple as that. 🤩

// before
import { ApiOkResponse, ApiProduces } from '@nestjs/swagger';

@ApiOkResponse({
  schema: {
    type: 'string',
    format: 'binary',
  },
})
@ApiProduces('image/png')
@Get('buffer')
buffer(@Res() response: Response) {
  const file = this.downloadService.imageBuffer();
  response.contentType('image/png');
  response.send(file);
}

// after
import { ApiFileResponse } from './api-file-response.decorator';

@ApiFileResponse('image/png')
@Get('buffer')
buffer(@Res() response: Response) {
  const file = this.downloadService.imageBuffer();
  response.contentType('image/png');
  response.send(file);
}

Sponsor us

Did you find this post useful? We at notiz.dev write about our experiences developing Apps, Websites and APIs and develop Open Source tools. Your support would mean a lot to us 🙏. Receive a reward by sponsoring us on Patreon or start with a one-time donation on GitHub Sponsors.

Table of Contents

Top of Page Comments Related Articles

Related Posts

Find more posts like this one.

Authors
Marc Stammerjohann
October 17, 2022

Codegen REST API types and requests for Angular

Automatic code generation from OpenAPI 3 for Angular
Angular NestJS Read More
Authors
Marc Stammerjohann
July 08, 2022

Maizzle: Craft beautiful HTML emails with Tailwind CSS

Send beautiful HTML emails via NestJS crafted with Maizzle and Tailwind CSS
Maizzle Tailwind CSS NestJS Read More
Authors
Marc Stammerjohann
August 26, 2021

NestJS: Type-safe File Uploads

Learn how to apply Swagger decorators for type-safe file upload endpoints.
NestJS Read More
Authors
Marc Stammerjohann
July 27, 2022

OpenAPI for your REST APIs in NestJS

Setup Swagger to generate an OpenAPI documentation for your REST endpoints.
NestJS Read More
Authors
Marc Stammerjohann
July 08, 2022

Send Emails with NestJS

Create Email Templates and send them with nodemailer from your Nest application
NestJS Read More
Authors
Marc Stammerjohann
September 12, 2022

Introducing NestJS Prisma Library and Schematics

Library and schematics to add Prisma integration to a NestJS application
NestJS Prisma Read More
Authors
Marc Stammerjohann
November 09, 2021

Dockerizing a NestJS app with Prisma and PostgreSQL

How to dockerize a NestJS application with Prisma and PostgreSQL.
NestJS Prisma Docker Read More
Authors
Marc Stammerjohann
April 07, 2020

GraphQL Code-First Approach with NestJS 7

Create a GraphQL API using Code-First Approach with NestJS 7.
NestJS GraphQL Prisma Read More

Sign up for our newsletter

Sign up for our newsletter to stay up to date. Sent every other week.

We care about the protection of your data. Read our Privacy Policy.