Luhn Algorithm Explained: Credit Card Validation in JavaScript

Published: (February 12, 2026 at 03:07 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

Luhn Algorithm – A Complete Card Validator

mnotr

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 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

Now change one digit — 4111 1111 1111 1112:

Sum: 31
31 % 10 !== 0 → Invalid

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)

That’s only 15 lines, no dependencies, and it runs in under a microsecond.


Detecting the Card Brand

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

BrandPrefix(es)Length(s)
Visa413, 16, 19
Mastercard51‑55, 2221‑272016
Amex34, 3715
Discover6011, 644‑649, 6516‑19
JCB3528‑358916‑19
UnionPay6216‑19
Diners Club300‑305, 36, 3814‑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';
}

Complete Validator

Combining Luhn check, brand detection, and length validation:

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

  if (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(),
  };
}

// Example
validateCard('4111-1111-1111-1111');
// → { valid: true, brand: "Visa", last_four: "1111", bin: "411111",
//     formatted: "4111 1111 1111 1111" }

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 any other gateway, saving you money (each processor call costs something) and giving users instant feedback.


Using an API Instead

If you prefer not to write the code yourself, you can call a ready‑made service:

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"
  }
}
*/

Or use the npm package:

npm install datacheck-api
import { validateCard } from "datacheck-api";

const result = await validateCard("4111111111111111");

Test Card Numbers

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

BrandNumber
Visa4111 1111 1111 1111
Mastercard5500 0000 0000 0004
Amex3782 822463 10005
Discover6011 1111 1111 1117
JCB3530 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 length checks for a complete client‑side validator, or delegate the work to an API if you prefer. Happy coding!

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.

0 views
Back to Blog

Related posts

Read more »

Server Components aren't SSR!

SSR vs. React Server Components In the dev world, React Server Components RSC are often mistaken for just another form of Server‑Side Rendering SSR. While both...

A Beginner’s Guide to ThreeJs

markdown ! https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2...