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
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 €'
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 }
And the values are branded types — not just strings:
type IBAN = string & { __brand: 'IBAN' }
type SortCode = string & { __brand: 'SortCode' }
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
}
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,
})
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>
)
}
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)
}
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 }, ...]
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, noNaN) - 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
- npm: npmjs.com/package/finprim
- GitHub: github.com/tintolee/finprim
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)