Framework Integration
A dependency-free, fluent client built on the Fetch API — works the same in Node and in the browser.
The client only needs the URL of your instance. In Node, read it from an environment variable:
# .env
PDF_GEN_API_URL=https://pdf.your-domain.dev/api/genIn the browser, there is no process environment, so pass the URL directly (e.g. from a public build-time constant such as NEXT_PUBLIC_PDF_GEN_API_URL).
⚠️ Don't use the demo instance (https://pdf.mathieutu.dev) in production: it is not versioned and can break without notice. Fork the project and deploy your own instance (Docker or Vercel).
This fluent client builds a POST /api/gen call from raw HTML and/or URLs (web pages, PDFs or images) to merge, using only the built-in fetch API. It runs unchanged in Node (18+), edge runtimes, and browsers.
export class PdfGenerator {
#html?: string
#urls: string[] = []
#filename?: string
constructor(private readonly apiUrl: string) {}
html(html: string): this {
this.#html = html
return this
}
url(url: string): this {
this.#urls.push(url)
return this
}
urls(urls: Iterable<string>): this {
for (const url of urls) this.url(url)
return this
}
filename(filename: string): this {
this.#filename = filename
return this
}
async generate(): Promise<ArrayBuffer> {
const response = await fetch(this.apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
html: this.#html,
urls: this.#urls,
filename: this.#filename,
}),
})
if (!response.ok) {
throw new Error(`Failed to generate PDF: ${await response.text()}`)
}
return response.arrayBuffer()
}
}Node: generate and save to disk
import { writeFile } from 'node:fs/promises'
import { PdfGenerator } from './pdf-generator'
const pdf = new PdfGenerator(process.env.PDF_GEN_API_URL!)
const buffer = await pdf
.html('<h1>Invoice #42</h1>')
.filename('invoice-42.pdf')
.generate()
await writeFile('invoice-42.pdf', Buffer.from(buffer))Node: return the PDF from an API route
// e.g. a Next.js API route or an Express handler
import { PdfGenerator } from './pdf-generator'
export async function GET() {
const buffer = await new PdfGenerator(process.env.PDF_GEN_API_URL!)
.url('https://example.com/report')
.generate()
return new Response(buffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="report.pdf"',
},
})
}Browser: generate and trigger a download
import { PdfGenerator } from './pdf-generator'
const pdf = new PdfGenerator('https://pdf.your-domain.dev/api/gen')
async function downloadInvoice(html: string) {
const buffer = await pdf.html(html).filename('invoice.pdf').generate()
const url = URL.createObjectURL(new Blob([buffer], { type: 'application/pdf' }))
const link = Object.assign(document.createElement('a'), { href: url, download: 'invoice.pdf' })
link.click()
URL.revokeObjectURL(url)
}Merge an HTML cover page with existing PDFs/URLs
const buffer = await new PdfGenerator(apiUrl)
.html('<h1>Cover page</h1>')
.urls(attachments.map(a => a.url))
.generate()| Method | API field | Description |
|---|---|---|
html(html) | html | Raw HTML content |
url(url) / urls(urls) | urls | A page, PDF or image URL, merged in the order it was added |
filename(name) | filename | File name attached to the generated document |
For direct file uploads (PDF/image/HTML as multipart/form-data, 4 MB max), see the multipart form data section of the project's README.

Trainer and pragmatic full-stack lead developer, currently open to short freelance assignments (let's talk!). Passionate about PHP and TypeScript, regular open-source contributor.
When I'm not in front of a screen, you'll usually find me cycling around Lyon, or in the surrounding mountains, where I spend a fair amount of time underground and under waterfalls...
pdf-gen is an open-source side project, born from the need for a simple, self-hostable API to generate and merge PDFs without vendor lock-in or per-document pricing.