Back to Blog
Web Development

Invoice Frontend: A Type-Safe React Invoicing Application

View Repository on GitHubLive Website / Demo

What Is Invoice Frontend?

invoice-frontend is the client-side application for a full-stack invoice management system. It pairs with invoice-backend — a Laravel REST API — to provide a complete invoicing workflow: creating clients, generating invoices, tracking payments, and exporting records.

Tech Stack

LayerTechnology
FrameworkReact 19 + TypeScript
Build ToolVite
UI Componentsshadcn/ui (components.json)
StylingTailwind CSS
FormsReact Hook Form + @hookform/resolvers
HTTP ClientAxios
Date Handlingdate-fns
Iconslucide-react
Linteroxlint
DeploymentVercel (vercel.json)

Project Structure

src/
├── pages/          # Route-level page components
├── components/     # Reusable UI components (shadcn/ui + custom)
├── contexts/       # React Context for auth and app state
├── lib/            # Axios instance, utility functions
├── data/           # Static/seed data
└── App.tsx         # Routes and layout

Why TypeScript?

This project uses TypeScript throughout — including strict tsconfig settings. The invoice domain has complex, interrelated data shapes: clients, line items, tax rates, payment statuses. TypeScript catches mismatches between the API response and the UI at compile time, rather than at runtime in production.

// Strict typing means the form knows exactly what shape invoice data takes
interface InvoiceLineItem {
  description: string;
  quantity: number;
  unitPrice: number;
  taxRate: number;
}

interface Invoice {
  id: string;
  clientId: string;
  lineItems: InvoiceLineItem[];
  status: 'draft' | 'sent' | 'paid' | 'overdue';
  issuedAt: string;   // ISO date string
  dueAt: string;
}

Form Handling with React Hook Form

Invoice forms are complex — line items can be added/removed dynamically, totals recalculate on every change, and everything needs validation before submission. React Hook Form handles this without re-rendering the entire form on each keystroke.

@hookform/resolvers connects the form to a Zod schema for declarative validation:

const invoiceSchema = z.object({
  clientId: z.string().min(1, 'Client is required'),
  dueAt: z.string().min(1, 'Due date is required'),
  lineItems: z.array(z.object({
    description: z.string().min(1),
    quantity: z.number().positive(),
    unitPrice: z.number().positive(),
  })).min(1, 'At least one line item is required'),
});

shadcn/ui Components

Rather than a traditional component library, this project uses shadcn/ui — copy-paste components built on Radix UI primitives and styled with Tailwind. The components.json in the root configures the component registry. This approach means the components live in the codebase and can be freely modified, unlike a locked-in external library.

Date Handling

Invoice dates are handled with date-fns for formatting, parsing, and arithmetic. Due date calculation (e.g., Net 30, Net 60) is computed client-side and sent to the backend as ISO strings.

Deployment

Deployed on Vercel with SPA rewrite rules in vercel.json to handle client-side routing correctly.

View on GitHub →