DEV Community

BillFast
BillFast

Posted on

Building a simple invoice PDF tool: the boring parts that matter

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
}
Enter fullscreen mode Exit fullscreen mode

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,
)
Enter fullscreen mode Exit fullscreen mode

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:

  1. render the invoice from the validated model
  2. keep print styles next to screen styles
  3. use a real print-sized page with predictable margins
  4. test long names, long addresses, three-digit quantities, and many line items
  5. 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)