PDFPipe

Guide

HTML to PDF in NestJS

The standard approach to PDF generation in a NestJS app adds a headless Chromium process (Puppeteer), a long-running binary (wkhtmltopdf), or a complex lifecycle wrapper. PDFPipe is an HTTP call. Wrap it in an injectable service, inject it wherever you need a PDF, and move on.

Injectable PDF service

Wrap the API call in a service so any module can inject it. The method returns a ReadableStream that you pipe directly to the response:

typescript
// pdf.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class PdfService {
  private readonly apiUrl = 'https://api.pdfpipe.xyz/v1/pdf';
  private readonly apiKey = process.env.PDFPIPE_API_KEY;

  async renderHtml(
    html: string,
    options: { format?: string; landscape?: boolean } = {},
  ): Promise<ReadableStream> {
    const res = await fetch(this.apiUrl, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ html, options: { format: 'A4', ...options } }),
    });

    if (!res.ok) {
      const err = await res.json().catch(() => ({}));
      throw new Error(err.detail ?? 'PDF render failed');
    }

    return res.body;
  }
}

Route controller

Inject the service into a controller and stream the PDF body straight to the client. No temp files, no disk writes:

typescript
// pdf.controller.ts
import { Controller, Get, Param, Res } from '@nestjs/common';
import { Response } from 'express';
import { PdfService } from './pdf.service';

@Controller()
export class PdfController {
  constructor(private readonly pdfService: PdfService) {}

  @Get('invoices/:id.pdf')
  async getInvoice(@Param('id') id: string, @Res() res: Response) {
    const html = `
      <html>
      <body style="font-family: system-ui; padding: 40px">
        <h1>Invoice #${id}</h1>
        <p>Thank you for your order.</p>
      </body>
      </html>
    `;

    const stream = await this.pdfService.renderHtml(html);

    res.setHeader('Content-Type', 'application/pdf');
    res.setHeader('Content-Disposition', `attachment; filename="invoice-${id}.pdf"`);

    // Pipe the ReadableStream (Web API) to the Express response.
    const { Readable } = await import('stream');
    Readable.fromWeb(stream as any).pipe(res);
  }
}

Module wiring

Register the controller and service in a module, then import PdfModule anywhere you need PDFs:

typescript
// pdf.module.ts
import { Module } from '@nestjs/common';
import { PdfController } from './pdf.controller';
import { PdfService } from './pdf.service';

@Module({
  controllers: [PdfController],
  providers: [PdfService],
  exports: [PdfService],
})
export class PdfModule {}

Store the PDF and return a URL

Pass store: true to keep the document on the CDN. Useful for background jobs that generate invoices and email a download link, or for any flow where you want to log the URL in your database and serve the PDF later:

typescript
// Store the PDF and return a download URL (useful for background jobs, emails).
@Injectable()
export class PdfService {
  async storeHtml(
    html: string,
    filename: string,
  ): Promise<{ docId: string; docUrl: string; expiresAt: string }> {
    const res = await fetch('https://api.pdfpipe.xyz/v1/pdf', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.PDFPIPE_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        html,
        options: { format: 'A4' },
        store: true,
        filename,
      }),
    });

    if (!res.ok) throw new Error('PDF render failed');

    return {
      docId: res.headers.get('X-PDFPipe-Document-Id') ?? '',
      docUrl: res.headers.get('X-PDFPipe-Document-Url') ?? '',
      expiresAt: res.headers.get('X-PDFPipe-Document-Expires') ?? '',
    };
  }
}

Getting an API key

The Hobby plan gives 500 free documents a month with no credit card required. Paid plans start at $19 and include a longer document archive, higher limits, and email support.

Full API reference on the docs page. Try it now in the live playground with no signup.

Get an API key →