Guide
HTML to PDF in Vue.js
Vue has no built-in PDF output. Client-side libraries that walk the DOM produce inconsistent results and cannot handle server-rendered pages. The right approach is a server-side HTTP call that returns a real PDF binary. PDFPipe works with any Node backend including Express or the Nitro server bundled with Nuxt.
Express backend route
For a Vue SPA backed by Express, add a route that builds your HTML, calls PDFPipe, and pipes the binary stream to the response. The Vue frontend simply navigates to the URL:
// server/routes/pdf.ts — Express backend for a Vue SPA
// npm install express
import express, { type Request, type Response } from "express";
const router = express.Router();
const API_URL = "https://api.pdfpipe.xyz/v1/pdf";
router.get("/invoices/:id/pdf", async (req: Request, res: Response) => {
const apiKey = process.env.PDFPIPE_API_KEY;
if (!apiKey) {
res.status(500).json({ error: "PDFPIPE_API_KEY not configured" });
return;
}
const { id } = req.params;
const html = buildInvoiceHtml(id);
const upstream = await fetch(API_URL, {
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(() => ({})) as Record<string, unknown>;
res.status(502).json({ error: String(err.detail ?? "PDF render failed") });
return;
}
res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Disposition", `attachment; filename="invoice-${id}.pdf"`);
// Pipe the upstream body to the Express response without buffering.
const reader = upstream.body!.getReader();
const pump = async () => {
const { done, value } = await reader.read();
if (done) { res.end(); return; }
res.write(value);
await pump();
};
await pump();
});
function buildInvoiceHtml(id: string): string {
return `
<!DOCTYPE html>
<html>
<body style="font-family: system-ui; padding: 40px">
<h1>Invoice #${id}</h1>
<p>Thank you for your purchase.</p>
</body>
</html>
`;
}
export default router;
// In your Express app entry point:
// app.use("/api", pdfRouter);Nitro server route (Nuxt 3)
Nuxt 3 uses Nitro as its server engine. Drop a file into server/routes/ and it is auto-registered. The h3 event handler calls PDFPipe and returns the stream, which Nitro pipes to the response:
// server/routes/pdf/[id].get.ts — Nitro server route (Nuxt 3)
// This file is auto-discovered by Nuxt; no registration required.
import { defineEventHandler, getRouterParam, createError } from "h3";
const API_URL = "https://api.pdfpipe.xyz/v1/pdf";
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id");
const apiKey = process.env.PDFPIPE_API_KEY;
if (!apiKey) throw createError({ statusCode: 500, message: "API key not configured" });
const html = `
<!DOCTYPE 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(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ html, options: { format: "A4" } }),
});
if (!upstream.ok) {
const body = await upstream.json().catch(() => ({})) as Record<string, unknown>;
throw createError({ statusCode: 502, message: String(body.detail ?? "PDF render failed") });
}
// Set response headers and return the readable stream.
event.node.res.setHeader("Content-Type", "application/pdf");
event.node.res.setHeader("Content-Disposition", `attachment; filename="invoice-${id}.pdf"`);
return upstream.body;
});
// Access via GET /api/pdf/42 from any Vue component:
// window.location.href = `/api/pdf/${invoiceId}`;Store and return a download URL
Pass store: true to keep the PDF on the CDN. The Nitro action returns the URL as JSON so your Vue component can open it in a new tab, display a link, or persist it to your database:
// server/routes/reports/generate.post.ts — Nitro action (Nuxt 3)
// Returns a JSON payload with the CDN URL instead of streaming the file.
import { defineEventHandler, readBody, createError, sendRedirect } from "h3";
const API_URL = "https://api.pdfpipe.xyz/v1/pdf";
export default defineEventHandler(async (event) => {
const { html, filename = "document.pdf" } = await readBody<{
html: string;
filename?: string;
}>(event);
if (!html) throw createError({ statusCode: 400, message: "html is required" });
const apiKey = process.env.PDFPIPE_API_KEY;
if (!apiKey) throw createError({ statusCode: 500, message: "API key not configured" });
const upstream = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
options: { format: "A4" },
store: true,
filename,
}),
});
if (!upstream.ok) {
throw createError({ statusCode: 502, message: "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");
// Return to the Vue component as JSON — let the client decide whether
// to redirect, open in a new tab, or store the URL for later.
return { docId, docUrl, expiresAt };
});
// Vue component usage:
// const { docUrl } = await $fetch("/api/reports/generate", {
// method: "POST",
// body: { html, filename: "report.pdf" },
// });
// window.open(docUrl, "_blank");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 →