Building one construction estimator is mostly basic geometry. Maintaining 76 independent material calculators—covering flooring, tile, roofing, concrete slabs, CMU block walls, stairs, and landscaping—without turning the codebase into copy-pasted spaghetti or paying for a backend API is a much more interesting TypeScript problem.

While building ProjectCalcs (Nuxt 4.2 + TypeScript + @nuxt/ui, statically prerendered to 255 routes on Cloudflare Pages via Nitro's cloudflare-pages-static preset), most of our real bugs weren't UI bugs—they were unit-boundary, rounding, and input-sanitization edge cases.

Here are three patterns that kept the math predictable across all calculators.

1. Normalize Units at the Input Boundary (and Guard Against Cleared Inputs)

Users measure rooms in feet, inches, meters, or centimeters—and frequently type partial decimals or clear an <input> back to 0 or empty while editing.

Instead of sprinkling conversion branches inside downstream cost and waste math, each calculator normalizes length inputs into a single canonical internal unit right at the top of a computed() block, while clamping denominators and openings before dividing:

const unitToFeet: Record<LengthUnit, number> = { ft: 1, in: 1 / 12, m: 3.28084, cm: 0.0328084, }; // Always normalize before computing area const lengthFt = roomLength * unitToFeet[roomUnit]; const widthFt = roomWidth * unitToFeet[roomUnit]; const roomAreaSqFt = lengthFt * widthFt; // Guard user-entered pack sizes so clearing an input never yields Infinity const safeTilesPerBox = tilesPerBox > 0 ? tilesPerBox : 10; 

On wall calculators with window/door deductions, clamping deducted area prevents negative surface areas when someone tests an oversized opening:

const sanitizedOpenings = Number.isFinite(input.openingsSqFt) ? Math.max(0, input.openingsSqFt) : 0; const openingsDeductedSqFt = Math.min(grossWallAreaSqFt, sanitizedOpenings); const wallAreaSqFt = grossWallAreaSqFt - openingsDeductedSqFt; 

2. Model Nominal vs. Actual Dimensions Explicitly

Material math breaks fastest when a nominal label looks like physical geometry. For example, in our Concrete Block Calculator, a standard 8" × 8" × 16" CMU block physically measures 7⅝" × 7⅝" × 15⅝". Because a standard 3/8" mortar joint restores the modular grid, the effective wall face area stays 128 sq in (0.889 sq ft, or 1.125 blocks/sq ft).

Rather than scattering magic numbers across templates and formulas, each CMU size carries its own yield and core-fill coefficients in a typed catalog:

export interface CMUSizeOption { value: string; nominalThickness: number; faceAreaSqIn: number; // 128 sq in including 3/8" mortar joint mortarYieldPerBag80: number; // blocks laid per 80-lb mortar bag fullGroutCuYdPer100SqFt: number; } 

Keeping mortar yields and hollow-cell grout fill factors bound to the block variant makes it trivial to keep related calculators consistent and testable with Vitest + fast-check.

3. Order Ceiling-Rounding Matters More Than Waste Percentage

One subtle UX bug shows up when combining waste allowance and discrete packaging (boxes of tile, bags of mortar, pallets, or bundles of shingles).

In our Tile Calculator, continuous area (per_sqft) scales with raw waste-adjusted square footage, whereas discrete quantities (per_tile, per_box) round up at each purchasing step so users don't end up short by two tiles on installation day:

const tilesNeeded = Math.ceil(roomAreaSqFt / tileAreaSqFt); const tilesWithWaste = Math.ceil(tilesNeeded * (1 + wastePercentage / 100)); const boxesNeeded = Math.ceil(tilesWithWaste / safeTilesPerBox); switch (priceUnit) { case "per_sqft": estimatedCost = roomAreaSqFt * (1 + wastePercentage / 100) * tilePrice; break; case "per_tile": estimatedCost = tilesWithWaste * tilePrice; break; case "per_box": estimatedCost = boxesNeeded * tilePrice; break; } 

Separating exact surface coverage from discrete packaged quantities avoids subtle under-ordering while still explaining every step transparently on the page.

Zero-Backend Static Prerendering

Because every calculation stays pure inside typed Vue composables, Nuxt prerenders all routes ahead of time (routeRules: { "/**": { prerender: true } }) into static HTML + client JS chunks with zero runtime server cost or user data leaving the browser.

If you've built engineering, finance, or CAD/takeoff calculators in TypeScript, I'd be curious how you handle imperial/metric fraction formatting and rounding edge cases in tests.