Guide
HTML to PDF in Bun
Bun has no native PDF library, and the npm-compatible packages that wrap Puppeteer or headless Chrome add a 150 MB binary that slows down your Docker builds and clashes with Bun's fast startup model. PDFPipe is a single HTTP call. Bun's fetch is built in and fast, so there is nothing to install.
Bun.serve handler
Build the HTML, POST it to PDFPipe, and stream the response body straight to the client:
// server.ts — run with: PDFPIPE_API_KEY=pp_live_... bun run server.ts
const apiKey = process.env.PDFPIPE_API_KEY!;
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
if (req.method === "GET" && url.pathname.startsWith("/invoices/")) {
const id = url.pathname.split("/").pop();
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 upstream = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ html, options: { format: "A4" } }),
});
if (!upstream.ok) {
const err = await upstream.json().catch(() => ({}));
return Response.json({ error: (err as any).detail ?? "render failed" }, { status: 502 });
}
// Pipe the upstream body directly to the client — no buffering.
return new Response(upstream.body, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="invoice-${id}.pdf"`,
},
});
}
return new Response("Not found", { status: 404 });
},
});With Elysia
Elysia is the most popular Bun-native framework. The same pattern works directly as a route handler:
// Elysia framework — npm install elysia (or bun add elysia)
import { Elysia } from "elysia";
const app = new Elysia()
.get("/invoices/:id.pdf", async ({ params }) => {
const html = buildInvoiceHtml(params.id); // your template function
const upstream = 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" } }),
});
if (!upstream.ok) throw new Error("PDF render failed");
return new Response(upstream.body, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="invoice-${params.id}.pdf"`,
},
});
})
.listen(3000);Store the PDF and return a URL
Pass store: true to persist the document on the CDN. The response headers contain the retrieval URL, document ID, and expiry time:
// Store the PDF and return a download URL.
const upstream = 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: `invoice-${orderId}.pdf`,
}),
});
if (!upstream.ok) throw new Error("PDF render failed");
const docId = upstream.headers.get("X-PDFPipe-Document-Id");
const docUrl = upstream.headers.get("X-PDFPipe-Document-Url");
const expiresAt = upstream.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 →