Why invoices are a good small-project problem
An invoice PDF tool sounds like a form plus a download button. In practice, it is a compact exercise in data modeling, validation, typography, and trust.
The happy path is easy: enter a client, add a few line items, and export a document. The useful product is the one that does not quietly change a total, lose a decimal, or produce a PDF that looks different from the preview.
1. Start with a boring, explicit data model
Keep the editable document separate from the rendered PDF. A minimal model can look like this:
type InvoiceItem = {
description: string
quantity: number
unitAmountMinor: number // pence/cents, never a float
}
type Invoice = {
number: string
issueDate: string // YYYY-MM-DD
dueDate?: string
currency: string
seller: Party
buyer: Party
items: InvoiceItem[]
notes?: string
}
The important detail is unitAmountMinor. Currency arithmetic with binary floating point is an unnecessary source of surprises. Store £12.50 as 1250, calculate in integers, and format only at the edge.
const subtotalMinor = items.reduce(
(sum, item) => sum + item.quantity * item.unitAmountMinor,
0,
)
If you support tax, make the rate and rounding rule explicit. “Tax included” and “tax added” are different invoices, not a formatting option.
2. Validate before rendering
Validate the same rules before an export or server-side render:
- invoice number is present and unique
- quantities are greater than zero
- prices use integer minor units
- required seller and buyer details are complete
- totals reconcile after tax and rounding
3. Use one source for preview and PDF
A common trap is building a beautiful HTML preview and then maintaining a second PDF template. They drift. A font changes in one place, a long address wraps in another, and the user downloads something they did not see.
For a small tool, HTML/CSS is often enough:
- render the invoice from the validated model
- keep print styles next to screen styles
- use a real print-sized page with predictable margins
- test long names, long addresses, three-digit quantities, and many line items
- export the same rendered document to PDF
If you need server-side PDFs, share the data model and formatting helpers, and make the layout testable without a browser.
4. Treat pagination as a product decision
Invoices become awkward when an item table crosses a page. Decide what should happen: repeat the table header on new pages, avoid splitting a single row where possible, keep totals together, and show a continuation marker.
5. Keep the workflow trustworthy
The PDF is not the product; confidence is. A few details help:
- show the invoice number and total in the preview
- make currency visible beside amounts
- use the same rounding in preview and export
- do not overwrite an existing draft accidentally
- make it clear whether data stays in the browser or is uploaded
- use a predictable filename such as
invoice-1042-client-name.pdf
For freelancers, “I can send this without checking it three times” is a better success metric than the number of form controls.
6. Test the edges before adding features
Before adding recurring invoices, accounts, or integrations, test the narrow workflow end to end: fill a realistic invoice, refresh where drafts matter, preview at 100% zoom, download the PDF, open it in two viewers, and verify the total independently.
That process tends to uncover more value than adding another settings screen.
A small, focused tool
I’m applying these ideas in BillFast, a lightweight invoice workflow for freelancers. If you want to try the result, it’s available at https://billfast-sandy.vercel.app — feedback on edge cases is welcome.
One more practical detail: invoice numbering
A numbering scheme is easier to trust when it is unique, sequential, and easy to explain. Decide whether you need a single sequence or separate prefixes for different businesses, avoid reusing numbers after an invoice is issued, and keep a clear record of voided numbers. A short checklist is here: https://billfast-sandy.vercel.app/guides/invoice-numbering
A small set of fixtures catches most regressions: one item, many items, a long description, a long address, and a second-page invoice.
A good error message identifies the field and the fix. “Invalid invoice” is not actionable; “Line 2 quantity must be greater than 0” is.
Top comments (0)