Week 8 of my NanoCrafts build curriculum had one goal: ship a second SaaS product fast. After spending seven weeks building Resume AI Tailor — a full-stack AI resume rewriter with Stripe, usage limits, and saved resumes — I wanted to validate that the patterns I'd built up weren't project-specific. Could I take the same stack and ship something completely different in a single week?
The invoice generator was the perfect test. Small scope, clear value, real utility. Type a sentence, get a professional PDF. No spreadsheets, no templates, no friction. Here's how I built it.
The NLP Parser — The Most Interesting Part
Every invoice generator has a form. Client name, hours, rate, currency — you fill it in, you get a PDF. Boring. The interesting question was: what if you didn't have to?
The core idea was simple. You type Invoice Acme for 10 hrs at £100/hr and the app figures out the rest. To make that work reliably, I built a two-tier parser.
Tier 1 — Regex (fast path)
The first tier is pure regex. No API call, no latency, no cost. It handles the patterns that cover the vast majority of real inputs:
Invoice Acme for 10 hrs at £100/hr
Bill TechCorp 5 hours at $75 per hour
Charge Wonka Co 3hrs £120/hr
Send invoice to Globex for 8 hours, rate £50
The regex extracts three things: client name, hours, and rate. Currency is inferred from the symbol — £ maps to GBP, $ to USD, € to EUR, with GBP as the default fallback. If all three fields are extracted successfully, the result comes back with confidence: "high" and the UI shows an "Auto-filled" badge in green.
Tier 2 — OpenAI fallback
If the regex fails — ambiguous phrasing, unusual structure, missing fields — the input gets passed to gpt-4o-mini with a tightly scoped system prompt:
Invoice Acme for 10 hrs at £100/hr
Bill TechCorp 5 hours at $75 per hour
Charge Wonka Co 3hrs £120/hr
Send invoice to Globex for 8 hours, rate £50
The model returns structured JSON, which gets parsed and returned with confidence: "low", triggering a yellow "AI-assisted" badge in the UI. The user sees the same form either way — they can always correct the fields before saving.
The confidence field is a small detail that pays off in UX. It tells the user exactly how much to trust the auto-fill without any extra explanation needed.
**
PDF Generation with @react-pdf/renderer
**
One of the advantages of building on a curriculum is that hard problems stay solved. I'd already integrated @react-pdf/renderer in Week 4 for Resume AI Tailor, so the mental model was already there — React components that describe a PDF layout, rendered server-side to a binary buffer and streamed back to the browser.
The invoice PDF is a single with one . The layout has five sections: a branded header with the NanoCrafts name and invoice number, a bill-to block with the client name and due date, a line items table with hours, rate, and amount columns, a totals block with subtotal, VAT at 20%, and grand total, and a payment terms footer.
The gotchas
Three things caught me that are worth documenting.
First, renderToBuffer is a named export, not a method on the default export. This sounds obvious but the library's own examples are inconsistent about it. The correct import is:
const { renderToBuffer } = await import('@react-pdf/renderer');
Not:
const { default: ReactPDF } = await import('@react-pdf/renderer');
await ReactPDF.renderToBuffer(...); // TypeError
Second, renderToBuffer returns a Node.js Buffer, which isn't directly assignable to the Response body in Next.js App Router. You need to convert it first:
const pdfBuffer = await renderToBuffer(element as any);
const uint8 = new Uint8Array(pdfBuffer);
return new Response(uint8, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${invoiceNumber}.pdf"`,
},
});
Third, @react-pdf/renderer must be in serverExternalPackages in next.config.ts, otherwise Next.js tries to bundle it client-side and throws. This is the same fix from Resume AI Tailor — it carried over directly.
The route
The PDF endpoint lives at GET /api/invoices/[id]/pdf. It fetches the invoice from Neon, checks ownership against the Clerk userId, renders the PDF server-side, and streams the bytes back. The download link on each invoice card is a plain pointing at this route — no JavaScript needed for the download itself.
Lessons vs Resume AI Tailor
The whole point of a curriculum is compounding. Each project should make the next one faster. Here's an honest breakdown of what carried over, what was new, and where I lost time anyway.
What carried over directly
The Clerk v7 setup was identical. proxy.ts instead of middleware.ts (a Next.js 16 convention change), async await auth(), protected routes via createRouteMatcher — copy, paste, done. Same for Drizzle ORM and Neon Postgres. The lib/db/index.ts singleton pattern, the drizzle.config.ts setup, even the drizzle-kit push workflow — all muscle memory by now.
@react-pdf/renderer was the biggest time save. Week 4 cost me several hours figuring out RSC conflicts, Buffer conversions, and the serverExternalPackages config. Week 8 cost me about twenty minutes. That's what compounding looks like in practice.
The Vercel deploy pipeline was also pre-solved. GitHub repo connected, environment variables via npx vercel env add, --prod flag. No surprises.
What was genuinely new
The NLP parser was the core new problem. Resume AI Tailor had no freetext input — everything came from structured file uploads. Building the two-tier regex and OpenAI fallback was the most interesting engineering work of the week.
Optimistic UI for the status changes was also new. Resume AI Tailor had no real-time state updates — results were fetched once and displayed. The invoice dashboard needed instant feedback when marking an invoice as sent or paid, which meant updating local state immediately and reverting on failure. It's a small pattern but a useful one to have in the toolkit.
Where I lost time anyway
Honestly, the biggest time sink of the week had nothing to do with the product. It was a Turbopack and Tailwind v4 workspace root detection bug in Next.js 16. When the project folder sits inside a parent directory that has its own package.json or package-lock.json, Turbopack walks up the tree and tries to resolve CSS imports from the wrong root. The fix was simple — scaffold with create-next-app into a clean location rather than cloning the previous project. That lesson cost about three hours.
The pattern I'd recommend: always start Week N fresh with create-next-app, then manually copy only the files you actually need from the previous project. It takes an extra thirty minutes but saves you from environment issues that are genuinely hard to debug.
T*ime saved overall*
My rough estimate is that reusing patterns from Resume AI Tailor saved around four hours this week. Authentication, database setup, PDF rendering, and deployment would each have taken significantly longer from scratch. That's the compounding effect in action — and it will only get stronger as the curriculum continues.
What I'd Do Differently
Every project teaches you something you wish you'd known at the start. Here are the three things I'd change if I built this again.
*1. Start fresh, don't clone *
I tried to save time by cloning Resume AI Tailor and stripping it down. It backfired. The nested folder structure triggered a Turbopack workspace detection bug that took hours to diagnose. Starting fresh with create-next-app would have taken thirty minutes and saved three hours. From now on, cloning is off the table. Copy individual files manually, never the whole project.
*2. Store line items in the database from day one *
The current schema stores a single hours and rate column per invoice — a simplification I made to ship faster. Line items are supported in the UI but only the first one gets persisted. In practice, almost every real invoice has multiple line items — design work, development, meetings billed separately. The fix is a separate line_items table with a foreign key to invoices. I'll add this in a future iteration, but it would have been cleaner to design it correctly from the start rather than retrofitting.
*3. Make VAT configurable *
VAT is hardcoded at 20% in the PDF component. That works for UK freelancers but breaks for everyone else — US freelancers don't charge VAT at all, EU rates vary by country. A simple vatRate field on the invoice with a default of 20 would have taken thirty minutes to add and made the product genuinely international from launch. Instead it's a v2 feature. The common thread across all three is the same: the shortcuts that feel like time savers at the start of the week tend to create friction at the end. The two that genuinely saved time — reusing Clerk and Drizzle patterns — were things I'd already invested in properly on a previous project. The shortcuts I took specifically for this project are the ones I'm now paying back.
The invoice generator is live at invoice-generator-six-roan-49.vercel.app and the code is open on GitHub at github.com/Azeez1314/invoice-generator. Week 9 of the NanoCrafts curriculum is already planned — if you want to follow along, I post build updates on LinkedIn and Twitter. And if you're a freelancer who invoices clients, give it a try and let me know what's missing.
Top comments (0)