DEV Community

mnotr
mnotr

Posted on • Originally published at datacheck.dev

Luhn Algorithm Explained: Credit Card Validation in JavaScript

Every credit card number has a built-in error-detection mechanism called the Luhn algorithm (also known as "modulus 10"). It catches accidental typos — swapped digits, single-digit errors, and most transposition mistakes — before a number ever reaches your payment processor.

This article explains how it works step by step, shows you a clean JavaScript implementation, and covers brand detection so you can build a complete card validator.

How the Luhn Algorithm Works

The algorithm operates on the card number's digits from right to left:

  1. Start from the rightmost digit (the check digit) and move left
  2. Double every second digit (starting from the second-to-last)
  3. If doubling produces a number > 9, subtract 9
  4. Sum all digits
  5. If the total is divisible by 10, the number is valid

Walk-Through: 4111 1111 1111 1111

This is Visa's standard test number. Let's trace through it:

Original:   4  1  1  1  1  1  1  1  1  1  1  1  1  1  1  1
Double alt: 8  1  2  1  2  1  2  1  2  1  2  1  2  1  2  1
Sum:        8+ 1+ 2+ 1+ 2+ 1+ 2+ 1+ 2+ 1+ 2+ 1+ 2+ 1+ 2+ 1 = 30
30 % 10 === 0 → Valid
Enter fullscreen mode Exit fullscreen mode

Now change one digit — 4111 1111 1111 1112:

Sum: 31
31 % 10 !== 0 → Invalid
Enter fullscreen mode Exit fullscreen mode

A single typo changes the checksum. That's the point.

JavaScript Implementation

function luhn(cardNumber) {
  const digits = cardNumber.replace(/\D/g, '');
  let sum = 0;
  let alternate = false;

  // Walk from right to left
  for (let i = digits.length - 1; i >= 0; i--) {
    let n = parseInt(digits[i], 10);

    if (alternate) {
      n *= 2;
      if (n > 9) n -= 9;
    }

    sum += n;
    alternate = !alternate;
  }

  return sum % 10 === 0;
}

// Test it
luhn('4111111111111111'); // true  (Visa test)
luhn('4111111111111112'); // false (one digit off)
luhn('5500000000000004'); // true  (Mastercard test)
luhn('378282246310005');  // true  (Amex test)
Enter fullscreen mode Exit fullscreen mode

That's 15 lines. No dependencies, no libraries. It runs in under 1 microsecond.

Detecting the Card Brand

Card brands are identified by their prefix (BIN range) and length:

Brand Prefix Length
Visa 4 13, 16, 19
Mastercard 51-55, 2221-2720 16
Amex 34, 37 15
Discover 6011, 644-649, 65 16-19
JCB 3528-3589 16-19
UnionPay 62 16-19
Diners Club 300-305, 36, 38 14-19
function detectBrand(cardNumber) {
  const d = cardNumber.replace(/\D/g, '');

  if (/^4/.test(d)) return 'Visa';
  if (/^5[1-5]/.test(d) || /^2[2-7]/.test(d)) return 'Mastercard';
  if (/^3[47]/.test(d)) return 'Amex';
  if (/^6(?:011|4[4-9]|5)/.test(d)) return 'Discover';
  if (/^35(?:2[89]|[3-8])/.test(d)) return 'JCB';
  if (/^62/.test(d)) return 'UnionPay';

  return 'Unknown';
}
Enter fullscreen mode Exit fullscreen mode

Complete Validator

Combining Luhn check, brand detection, and length validation:

function validateCard(input) {
  const digits = input.replace(/\D/g, '');

  if (digits.length < 13 || digits.length > 19) {
    return { valid: false, reason: 'Card number must be 13-19 digits' };
  }

  if (!luhn(digits)) {
    return { valid: false, reason: 'Invalid checksum (Luhn check failed)' };
  }

  const brand = detectBrand(digits);
  const lastFour = digits.slice(-4);
  const bin = digits.substring(0, 6);

  return {
    valid: true,
    brand,
    last_four: lastFour,
    bin,
    formatted: digits.replace(/(.{4})/g, '$1 ').trim(),
  };
}

validateCard('4111-1111-1111-1111');
// { valid: true, brand: "Visa", last_four: "1111", bin: "411111",
//   formatted: "4111 1111 1111 1111" }
Enter fullscreen mode Exit fullscreen mode

What Luhn Doesn't Catch

The Luhn algorithm only checks for accidental errors. It does not:

  • Verify the card exists — you need a payment processor for that
  • Check if the card is expired — validate expiry separately
  • Detect stolen numbers — that's fraud detection, not validation
  • Catch all transposition errors — swapping 09 and 90 passes Luhn

Think of Luhn as a fast pre-filter. It rejects obvious mistakes before you make an API call to Stripe or your payment gateway. This saves you money (each processor call costs something) and gives users instant feedback.

Using an API Instead

If you want Luhn validation + brand detection + formatting without writing the code yourself:

const res = await fetch(
  'https://datacheck.dev/api/validate?input=4111111111111111&type=credit_card'
);
const data = await res.json();

// {
//   valid: true,
//   formatted: "4111 1111 1111 1111",
//   details: {
//     brand: "Visa",
//     last_four: "1111",
//     bin: "411111",
//     type: "credit"
//   }
// }
Enter fullscreen mode Exit fullscreen mode

Or with the npm package:

npm install datacheck-api
Enter fullscreen mode Exit fullscreen mode
import { validateCard } from "datacheck-api";
const result = await validateCard("4111111111111111");
Enter fullscreen mode Exit fullscreen mode

Test Card Numbers

Use these for testing. They all pass Luhn but aren't real cards:

Brand Number
Visa 4111 1111 1111 1111
Mastercard 5500 0000 0000 0004
Amex 3782 822463 10005
Discover 6011 1111 1111 1117
JCB 3530 1113 3330 0000

Wrapping Up

The Luhn algorithm is one of the most useful 15-line functions you'll ever write. It catches typos instantly, works offline, and costs nothing to run. Pair it with brand detection and you've got a complete client-side card validator.

For production apps, use it as a pre-filter before calling your payment processor — or use an API like DataCheck that handles everything in one call.

Top comments (0)