Skip to content
Devix Open Source

Guide

Getting started

npm install @devix-labs/validators
import { validate } from '@devix-labs/validators';

const result = validate('iban', 'AE07 0331 2345 6789 0123 456');

Every answer has the same shape:

{
  valid: true,
  reason: 'valid',              // or 'too-short', 'bad-checksum', 'unknown-country'…
  kind: 'iban',
  value: 'AE070331234567890123456',        // cleaned: separators gone, upper-cased
  formatted: 'AE07 0331 2345 6789 0123 456',
  checked: 'checksum',          // or 'structure' — see below
  country: 'AE',
  details: { bankCode: '033', bban: '0331234567890123456', checkDigits: '07' },
}

checked is the important field

A check digit can catch a typo. A number without one cannot be checked beyond its shape — and saying so is the difference between honest validation and a false sense of safety:

validate('emirates-id', '784199012345676').checked;   // 'checksum' — a typo is caught
validate('uae-trn', '100123456789012').checked;       // 'structure' — the FTA publishes no check digit

Never tell someone their TRN is "valid". Tell them it looks right.

What it knows

Kind Number Checked
iban IBAN, all 78 registry countries checksum (MOD 97-10)
vat European VAT, 28 countries checksum for 15, structure for the rest
emirates-id Emirates ID checksum (Luhn)
uae-trn UAE Tax Registration Number structure
saudi-id Saudi national ID or Iqama checksum (Luhn)
saudi-vat Saudi VAT number structure
qatar-id Qatar ID structure
kuwait-id Kuwait Civil ID checksum (weighted mod 11)
bahrain-cpr Bahrain CPR structure
oman-id Oman civil number structure
cnic Pakistani CNIC structure
ntn Pakistani NTN structure
pan Indian PAN structure
gstin Indian GSTIN checksum (base-36)

VAT check digits are verified for AT, BE, DE, DK, FI, FR, GB, IE, IT, LU, NL, PL, PT, SE and SK.

Reason codes

Reason Means
empty Nothing was typed.
too-short / too-long The wrong number of characters for that country or kind.
bad-characters Letters where digits belong, or characters outside the alphabet.
bad-structure The right length, the wrong shape — a CNIC starting with 9, a GSTIN with no state.
bad-checksum The check digit disagrees: almost always a typo or a transposition.
unknown-country We have no rules for that country, or you limited it to others.
unsupported No validator by that name.

Turning a reason into a message

const MESSAGES = {
  empty: 'Please enter your IBAN.',
  'too-short': 'That IBAN is too short — check you copied all of it.',
  'bad-checksum': 'That IBAN has a digit wrong. Check it against your bank statement.',
  'unknown-country': 'We can only take an IBAN from the UAE or Saudi Arabia.',
};

const result = validate('iban', input.value, { only: ['AE', 'SA'] });
if (!result.valid) show(MESSAGES[result.reason] ?? 'That does not look like an IBAN.');

One validator at a time

Import only what you use — the package is side-effect free, so nothing else ships:

import { iban, emiratesId } from '@devix-labs/validators';

Formatting, masks and test data

import { format, mask, generate } from '@devix-labs/validators';

format('iban', 'ae070331234567890123456');   // 'AE07 0331 2345 6789 0123 456'
format('cnic', '4210112345671');             // '42101-1234567-1'
mask('cnic');                                // '99999-9999999-9'  (9 digit, A letter, * either)
generate('emirates-id');                     // a valid Emirates ID that belongs to nobody
generate('iban', { country: 'SA', seed: 7 });// deterministic, for fixtures

generate() is for seeders, fixtures and demos. The numbers pass their own check digits and are not issued to anyone.

On the server

devix-labs/laravel-validators is the same rules in PHP, with Laravel rules and messages:

composer require devix-labs/laravel-validators
$request->validate([
    'iban' => 'iban:AE,SA',
    'emirates_id' => 'emirates_id',
]);

224 shared vectors assert that both sides answer identically — including the reason code.

Updated 15 Sep 2026