DEV Community

Cover image for Stop reinventing financial validation in TypeScript
Oluwatosin Adelaja
Oluwatosin Adelaja

Posted on

Stop reinventing financial validation in TypeScript

If you've worked on a payment product, a banking app, or anything involving money, you've written this code before.

An IBAN validator you copied from Stack Overflow. A Luhn check for card numbers someone found in a gist. A currency formatter that works in en-US but breaks for de-DE. A sort code regex that someone on the team "maintains." A loan calculator buried in a utility file no one touches.

Every fintech team builds this stuff. Then rebuilds it. Then inherits the third version and tries to figure out why it exists.

I built finprim to fix this.


What finprim is

finprim is a zero-dependency TypeScript library that handles the financial primitives every app needs:

  • IBAN validation (80+ countries) and formatting
  • BIC/SWIFT validation
  • UK sort code and account number validation
  • Card number validation (Luhn algorithm + network detection)
  • EU VAT number validation
  • US ABA routing number validation
  • Loan/EMI calculations with full amortization schedules
  • Multi-locale currency formatting and parsing

One library. Consistent API. Fully typed.

npm install finprim
Enter fullscreen mode Exit fullscreen mode
import { validateIBAN, validateCardNumber, formatCurrency } from 'finprim'

validateIBAN('GB29NWBK60161331926819')
// { valid: true, value: 'GB29NWBK60161331926819', formatted: 'GB29 NWBK 6016 1331 9268 19', countryCode: 'GB' }

validateCardNumber('4532015112830366')
// { valid: true, formatted: '4532 0151 1283 0366', network: 'Visa', last4: '0366' }

formatCurrency(1000.5, 'EUR', 'de-DE')
// '1.000,50 €'
Enter fullscreen mode Exit fullscreen mode

No config. No setup. Just import and use.


The TypeScript part you'll actually care about

Here's where it gets good. Every validator returns a discriminated union:

type ValidationResult<T> =
  | { valid: true; value: T; formatted: string }
  | { valid: false; error: string }
Enter fullscreen mode Exit fullscreen mode

And the values are branded types — not just strings:

type IBAN = string & { __brand: 'IBAN' }
type SortCode = string & { __brand: 'SortCode' }
Enter fullscreen mode Exit fullscreen mode

This means TypeScript will catch you if you try to pass an unvalidated string where a validated IBAN is expected:

function processPayment(iban: IBAN, amount: number) {
  // ...
}

// This won't compile — userInput is just a string
processPayment(userInput, 100) // Error!

// You must validate first
const result = validateIBAN(userInput)
if (result.valid) {
  processPayment(result.value, 100) // result.value is typed as IBAN
}
Enter fullscreen mode Exit fullscreen mode

This is the kind of thing that prevents entire categories of production bugs.


Works with your existing stack

The core is zero-dependency. But if you're using Zod, React, or NestJS, there are optional integrations:

Zod

import { z } from 'zod'
import { ibanSchema, sortCodeSchema, currencySchema } from 'finprim/zod'

const PaymentSchema = z.object({
  iban: ibanSchema,
  sortCode: sortCodeSchema,
  amount: z.number().positive(),
  currency: currencySchema,
})
Enter fullscreen mode Exit fullscreen mode

React hooks for form inputs

import { useIBANInput, useCardNumberInput } from 'finprim/react'

function PaymentForm() {
  const iban = useIBANInput()
  const card = useCardNumberInput()

  return (
    <form>
      <input
        value={iban.value}
        onChange={iban.onChange}
        aria-invalid={iban.valid === false}
        placeholder="GB29 NWBK 6016 1331 9268 19"
      />
      <input
        value={card.formatted}
        onChange={card.onChange}
        aria-invalid={card.valid === false}
        placeholder="4532 0151 1283 0366"
      />
    </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

The hook formats as you type and validates in real time.

NestJS pipes

import { IbanValidationPipe } from 'finprim/nest'

@Get('account/:iban')
findByIban(@Param('iban', IbanValidationPipe) iban: string) {
  return this.service.findByIban(iban)
}
Enter fullscreen mode Exit fullscreen mode

Loan calculations

Financial apps often need EMI calculations. This one's baked in:

import { calculateEMI, getLoanSchedule } from 'finprim'

// Monthly payment on a £100,000 loan at 10% APR over 12 months
const monthly = calculateEMI(100_000, 10, 12)

// Full amortization schedule
const schedule = getLoanSchedule(100_000, 10, 12)
// [{ month: 1, payment: 8791.59, principal: 8124.92, interest: 833.33, balance: 91875.08 }, ...]
Enter fullscreen mode Exit fullscreen mode

Production-grade by default

A few things that matter in real apps:

  • All string validators reject inputs longer than 256 chars (no unbounded input processing)
  • Numeric helpers validate bounds (no Infinity, no NaN)
  • The library never logs or persists input data
  • Format helpers are safe to call without validation (useful for display-only flows)

Try it

npm install finprim
Enter fullscreen mode Exit fullscreen mode

If it saves you time, a star on GitHub goes a long way. And if you find an edge case or want to add a validator, PRs are open.

Top comments (0)