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.