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:
// 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:
// 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:
// 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:
// 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 →