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.