DEV Community

Jonas Hämmerle
Jonas Hämmerle

Posted on

Why your regex for IBAN validation is probably wrong

A regex like /^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/ will pass through plenty of IBANs that are complete garbage. It checks the shape — two letters, two digits, then alphanumerics — but shape isn't validity, and this is the mistake nearly every from-scratch IBAN validator makes.

Here's what a shape-only regex misses, in order of how often it bites people:

1. Country-specific length. IBAN length isn't a range, it's a fixed number per country: Germany is always 22 characters, the Netherlands 18, Malta 31. A regex with {11,30} for the BBAN portion accepts a "German" IBAN that's the wrong length for Germany, because it's only checking the global range across all countries, not the specific one this IBAN claims to be from.

2. The checksum. This is the part a regex structurally cannot do — regular expressions can't compute a mod-97 checksum, because that requires arithmetic over the whole string, not pattern matching. Every IBAN has two check digits (positions 3-4) that are the result of ISO 7064's mod-97-10 algorithm applied to the rearranged, letter-to-number-converted account number. A regex will happily accept an IBAN with those two digits set to anything:

function isValidIBAN(iban) {
  const clean = iban.replace(/\s+/g, "").toUpperCase();
  if (!/^[A-Z]{2}\d{2}[A-Z0-9]+$/.test(clean)) return false; // shape check — necessary but nowhere near sufficient

  const rearranged = clean.slice(4) + clean.slice(0, 4);
  const numeric = rearranged.replace(/[A-Z]/g, c => (c.charCodeAt(0) - 55).toString());

  let remainder = numeric;
  while (remainder.length > 2) {
    const chunk = remainder.slice(0, 9);
    remainder = (parseInt(chunk, 10) % 97) + remainder.slice(chunk.length);
  }
  return parseInt(remainder, 10) % 97 === 1;
}
Enter fullscreen mode Exit fullscreen mode

Note the chunked-modulo loop — IBANs convert to numbers with 30+ digits, well past what JS's Number type can hold precisely, so you can't just do bigNumber % 97 in one step without either a BigInt polyfill or this kind of iterative reduction.

3. Per-country BBAN structure. Beyond length, each country also defines which positions must be digits vs letters within the BBAN. A regex checking "alphanumeric" for the whole remainder will accept a UK IBAN with digits where the bank code letters should be.

None of this means "validate against a live bank lookup" — that's a different, heavier problem (does the account exist right now), and format validation deliberately doesn't answer it. It means: regex for shape, then the real mod-97 checksum, then country-specific length/structure — in that order, each one catching what the previous step can't.

I wrapped all three checks into one endpoint on Validate if you'd rather not own the mod-97 edge cases and the per-country BBAN table yourself. Same account also covers QR API and Currency API if useful.

Top comments (0)