Ivoice Generator
How I Built Real Vector PDF Invoices in My SaaS — React Frontend + NestJS Backend
No canvas. No screenshots. No
html2canvas. Just clean, selectable, zoomable, printable PDFs — exactly like Tally or Excel generates them.
I'm building a full-stack event management SaaS called HostWithMe where vendors can generate GST-compliant Tax Invoices for their clients. The invoice had to feel professional — the kind a CA would accept, not a screenshot stapled to an email.
This post walks through the entire implementation: the thinking behind the approach, the React PDF layout, the NestJS API, the logo embedding fix, and every gotcha I hit along the way.
The Problem with the "Easy" Approaches
When most developers need to generate a PDF from a React page, they reach for one of these:
window.print()with print CSShtml2canvas→jsPDFPuppeteer to screenshot the page
These all have the same fatal flaw: they generate image-based PDFs. The text inside is not real text — it's pixels baked into an image layer. You can't:
Select or copy text
Search inside the PDF
Zoom in without it blurring
Have screen readers parse it
That's not a PDF. That's a fancy screenshot.
What I needed was what Tally, QuickBooks, and Excel produce — a vector PDF where every character is a real typographic glyph, every line is a real vector path, and the file is a true document.
The Right Tool: @react-pdf/renderer
@react-pdf/renderer@react-pdf/renderer is a React renderer that targets PDFKit instead of the DOM. You write JSX, but instead of HTML elements it has its own primitives: <Document>, <Page>, <View>, <Text>, <Image>. It compiles your component tree into a real .pdf file with embedded fonts and vector graphics.
The key mental shift: this is not a styled HTML page. You're writing a layout engine for print. No Tailwind, no CSS classes — everything is inline StyleSheet objects, similar to React Native.
Architecture Overview
Here's how the full system is wired together:
Two separate concerns:
The PDF Document component — pure layout, no side effects
The API layer — data fetching, image resolution, streaming
Let me walk through both.
Part 1: The React PDF Document Component
The Layout Philosophy
I needed to match this exact invoice structure:
File: InvoicePDFDocument.tsx
InvoicePDFDocument.tsxKey Patterns in @react-pdf/renderer
@react-pdf/renderer1. <View> is your <div>
Everything is a View. Flex is the only layout model — and it defaults to flexDirection: "column", unlike CSS which defaults to row.
2. Tables are just nested Views with borders
There's no <table> element. You fake it with borders:
3. Bold text requires a different fontFamily
There's no fontWeight: "bold" that just works. You must switch the fontFamily:
Built-in fonts available: Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique, and the same variants for Courier and Times-Roman.
4. The <Image> component for logos
More on why logoBase64 and not a URL — see Part 3.
5. The Grand Total row — dark background
The HSN Summary Block
This is an important GST requirement — a control block at the bottom grouping tax amounts by HSN/SAC code. I built it by reducing the line items:
Amount in Words — Indian Number System
Standard number-to-words doesn't handle Indian numbering (Lakhs, Crores). I wrote a custom converter:
Part 2: The NestJS Backend
In my stack, the PDF is generated server-side via a NestJS API. This is the recommended approach because:
renderToBuffer()runs in Node — no browser neededYou can pre-process images, fetch data, and validate before rendering
You can also store the PDF to S3 / send via email in the same flow
Controller
Service
Note for Next.js users: If you're not using NestJS, the equivalent is an App Router API route (
app/api/invoice/pdf/route.ts) withrenderToBuffer()called inside aPOSThandler. The logic is identical — only the HTTP wrapper differs.
Part 3: The Logo Problem (and Why It's Subtle)
This is the part that caught me off guard and took me a while to debug.
What I tried first
The logo showed perfectly in the browser preview (using a regular <img> tag). But in the generated PDF — blank. Just empty space where the logo should be.
Why it fails
@react-pdf/renderer runs on the Node.js server, not in the browser. When it tries to fetch a remote URL like a Cloudinary image:
CORS: Cloudinary and other CDNs set CORS headers for browser requests. Node's fetch is not a browser — it sends different headers and can get blocked or silently fail
No browser cache: The image isn't pre-loaded like it would be in a browser
Timing:
@react-pdf/rendererdoesn't always await remote image fetches reliably
The fix: pre-fetch and convert to base64
The solution is to fetch the image yourself, on the server, before calling renderToBuffer. Then pass the image as a base64 data URI — no network call needed during rendering.
Then in the PDF component:
This is the same reason tools like Puppeteer and wkhtmltopdf need --allow-local-file-access or explicit network waiting — rendering engines don't always behave like a real browser loading a page.
Part 4: The React Frontend Integration
On the frontend, two things exist side by side:
The HTML preview — your existing React component, rendered normally in the browser using Tailwind/CSS — this is what the user sees on screen
The PDF download — calls the backend API, gets a binary blob, triggers a download
They're completely independent. The HTML preview doesn't need to match the PDF layout pixel-perfectly — they're built with different tools for different purposes.
The download utility
The button in the component
Preview in new tab instead of downloading:
Complete Data Flow — End to End
Gotchas Summary
Logo missing in PDF
Remote URL fetch fails in Node / CORS
Pre-fetch to base64 before renderToBuffer
fontWeight: "bold" not working
@react-pdf has limited CSS support
Use fontFamily: "Helvetica-Bold"
display: flex not needed
@react-pdf defaults to column flex
Use flexDirection: "row" explicitly
₹ rupee symbol not rendering
Built-in fonts don't have all Unicode
Register a custom font (Noto Sans)
PDF cuts off mid-page
Fixed height containers overflow
Use minHeight, avoid fixed heights on Page
Next.js SSR error with @react-pdf
Library uses Node APIs
Only import in API routes or "use server" files
Canvas error in Next.js
Optional peer dependency
Add canvas: false to webpack aliases
Fixing the Rupee symbol (if needed)
If your ₹ renders as a box or empty:
Next.js webpack config fix
What I'd Do Differently
For scale: Move PDF generation to a dedicated microservice or a background job queue (BullMQ). Generating PDFs is CPU-bound and you don't want it blocking your main NestJS event loop under load.
For logos: Store a pre-processed base64 version of the logo in the vendor document at upload time — skip the runtime fetch entirely.
For complex layouts: Consider pdfmake if you need more control over page breaks, multi-page tables, or watermarks. It has a lower-level API but is more predictable for complex documents.
For email delivery: After renderToBuffer, you can directly attach the buffer to a Nodemailer email:
Final Thoughts
The moment I opened the generated PDF and could select the invoice number, copy the vendor address, and Ctrl+F search for the client name — that was the payoff. It felt like a real document, not a hack.
The key lessons:
@react-pdf/rendereris the right tool for vector PDFs in a JS/TS stack. It's mature, actively maintained, and the output quality is excellent.Never pass remote image URLs to the renderer. Always pre-fetch and convert to base64 server-side.
The HTML preview and the PDF are separate concerns. Don't try to share the same component for both — they're built for completely different rendering engines.
Think print-first. Minimal color, clean borders, readable at 100% zoom and at A4 print size.
The full source for InvoicePDFDocument.tsx, the NestJS service, and the client utility function are available in the project repo.
Building HostWithMe — an event management platform for Indian vendors. Follow along for more posts on GST compliance, full-stack architecture, and shipping fast.
Tags: React NestJS PDF TypeScript GST Invoice @react-pdf/renderer Full Stack
Last updated