Building a Next.js Invoice Generator with PDF Export
Step-by-step overview of building an invoice generator in Next.js and exporting invoices as PDFs using Puppeteer.
Invoices are a common requirement for SaaS apps, freelancing platforms, and internal tools. With Next.js, you can build a full-stack invoice generator that combines a React UI with server-side PDF rendering.
One practical approach is to use a simple HTML+CSS invoice template, then render it in a headless browser like Puppeteer and export it as a PDF. If you want to skip the implementation and use a ready-made solution, try the Invoice Generator tool on jsgenerator.com at https://jsgenerator.com/tools/invoice-generator.
Why Choose Next.js + Puppeteer:
Full-stack Integration: Next.js allows us to easily create both the frontend form and backend API within the same project.
Server-side Rendering Advantages: Generating PDFs on the server ensures data consistency, avoids client-side environment differences, and can handle sensitive information without exposing it to the client.
Precise Print Control: HTML and CSS (especially print styles) provide unparalleled layout control, allowing us to precisely match physical invoice styling requirements.
Puppeteer's Reliability: Puppeteer is a powerful Node.js library that controls headless Chrome, capable of perfectly converting web pages into precisely laid-out PDF files.
System Architecture Overview:
A complete invoice generator typically follows this workflow:
1. Frontend (React UI): A dynamic form for collecting invoice data (such as company info, items, tax rates, etc.).
2. Backend (Next.js API Route): Receives the JSON data submitted from the frontend.
3. Template Engine: Injects the JSON data into a predefined HTML invoice template.
4. PDF Generation (Puppeteer): Launches a headless browser instance, loads and renders the HTML, and exports it as a PDF file.
5. File Delivery: Streams the generated PDF back to the frontend for user download.
Step-by-Step Implementation Guide:
Step 1: Create the Invoice Editing Form:
We first create a page in Next.js containing a form for entering all invoice details.
// pages/invoice-creator.js
import { useState } from 'react';
export default function InvoiceCreator() {
const [from, setFrom] = useState('');
const [to, setTo] = useState('');
const [items, setItems] = useState([{ description: '', quantity: 1, price: 0 }]);
const addItem = () => {
setItems([...items, { description: '', quantity: 1, price: 0 }]);
};
const generatePdf = async () => {
// Send data to API route
const response = await fetch('/api/generate-pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ from, to, items }),
});
if (response.ok) {
// Get PDF blob and trigger download
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `invoice-${Date.now()}.pdf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
} else {
console.error('PDF generation failed');
}
};
return (
<div>
<h1>Create Invoice</h1>
{/* Detailed form controls here */}
<button onClick={generatePdf}>Generate PDF Invoice</button>
</div>
);
}Step 2: Design the HTML Invoice Template:
This is the core part. We create a standalone HTML file (or a React component that returns JSX) as a template. It uses Tailwind CSS or plain CSS to define the invoice styling.
// templates/invoice-template.js
export const InvoiceTemplate = ({ data }) => {
const total = data.items.reduce((sum, item) => sum + item.quantity * item.price, 0);
return (
<html>
<head>
<meta charSet="utf-8" />
<title>Invoice</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body className="p-10">
<div className="max-w-4xl mx-auto bg-white p-8 border border-gray-300">
<h1 className="text-3xl font-bold mb-10">INVOICE</h1>
<div className="flex justify-between mb-10">
<div>
<h2 className="font-bold">From:</h2>
<p>{data.from}</p>
</div>
<div>
<h2 className="font-bold">To:</h2>
<p>{data.to}</p>
</div>
</div>
<table className="w-full mb-10">
<thead>
<tr className="border-b-2 border-gray-300">
<th className="text-left">Description</th>
<th className="text-right">Qty</th>
<th className="text-right">Price</th>
<th className="text-right">Total</th>
</tr>
</thead>
<tbody>
{data.items.map((item, index) => (
<tr key={index} className="border-b border-gray-200">
<td>{item.description}</td>
<td className="text-right">{item.quantity}</td>
<td className="text-right">${item.price.toFixed(2)}</td>
<td className="text-right">${(item.quantity * item.price).toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
<div className="text-right text-xl font-bold">
Total: ${total.toFixed(2)}
</div>
</div>
</body>
</html>
);
};Step 3: Implement the PDF Generation API Route:
In the Next.js API route, we integrate Puppeteer to render the template and generate the PDF.
// pages/api/generate-pdf.js
import { InvoiceTemplate } from '../../templates/invoice-template';
import ReactDOMServer from 'react-dom/server';
import puppeteer from 'puppeteer-core';
// Note: In production, you might want to use `puppeteer-core` and connect to a remote Chrome instance.
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).end();
}
const { from, to, items } = req.body;
try {
// 1. Render the React template to an HTML string
const htmlContent = ReactDOMServer.renderToStaticMarkup(
InvoiceTemplate({ data: { from, to, items } })
);
// 2. Launch Puppeteer
const browser = await puppeteer.launch({
executablePath: process.env.CHROME_PATH, // In production, point to your Chrome path
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page = await browser.newPage();
// 3. Set HTML content and wait for all resources to load
await page.setContent(htmlContent, { waitUntil: 'networkidle0' });
// 4. Generate PDF
const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' },
});
await browser.close();
// 5. Return the PDF to the client
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename="invoice.pdf"');
res.send(pdfBuffer);
} catch (error) {
console.error('PDF generation error:', error);
res.status(500).json({ error: 'Failed to generate PDF' });
}
}Production Environment Optimization and Considerations:
Chrome Management: In Serverless environments like Vercel, launching Chrome directly may not be feasible. Solutions include using adapters like `@sparticuz/chromium` or connecting to a standalone Chrome rendering service.
Performance: Launching a browser instance has significant overhead. Consider reusing browser instances or using connection pools.
Security: Strictly validate and sanitize user input to prevent HTML injection attacks.
Style Consistency: Ensure CSS used for PDF generation is inline or self-contained, avoiding dependencies on external network resources.
Standing on the Shoulders of Giants:
As demonstrated by the Invoice Generator on jsgenerator.com, a production-ready solution is more than just a simple technical stack. It requires:
Intuitive UI: Guide users to easily enter all necessary information.
Flexible Data Model: Support complex scenarios such as taxes, discounts, and multiple currencies.
Robust API: Handle various edge cases and high-concurrency requests.
Optimized Output: Ensure the generated PDF files are small, high-quality, and professionally formatted.
By understanding and implementing the above architecture, you can build a powerful invoice generation system integrated into your Next.js application. For developers who want to quickly obtain mature, stable features along with copy-paste ready code snippets, leveraging professional tools like jsgenerator is a wise move to accelerate the development process.