Learn
🤖 AI Hub 📰 News 📖 Dictionary 🧰 Resources
Grow
💼 Jobs 🚀 Community 🏆 Challenges
Explore by Stage
🌑 Zero 📚 Learn 🔧 Practice 🏗 Build 🚀 Launch 💰 Earn
Account
Login Sign Up
🏗️HTML 🎨CSS ⚡JavaScript 🐙Git & GitHub ⚛️React Soon 📘TypeScript Soon 🐍Python 🗄️Database & SQL Soon 🌐Web Development Soon ☁️Cloud & DevOps Soon 🤖AI & ML Soon 📄Resume Prep Soon 🎤Interview Prep Soon 💼Freelancing Soon 🚀SaaS & Side Projects Soon 🤖Automation Soon
Advanced 🕑 13 min read 👁 2 views 📅 Oct 21, 2026

Regular Expressions: Checking PIN Codes, Mobile Numbers and More

Learn JavaScript regular expressions by validating a KYC form: PIN codes, mobile numbers, PAN and vehicle numbers. Use test, match, matchAll, replace, groups and flags, building each pattern step by step.

The KYC Form Problem

Every bank, mobile wallet and broker app in India asks you to complete KYC (Know Your Customer). You type your PIN code, mobile number, PAN and sometimes your vehicle number. Each one has a strict format. A PIN code has 6 digits and never starts with 0. A mobile number has 10 digits and starts with 6, 7, 8 or 9. A PAN looks like ABCDE1234F: five letters, four digits, one letter. A vehicle number looks like MH12AB1234.

You could check these with loops and many if statements, character by character. It would take dozens of lines for each field. A regular expression (regex for short) describes the whole format in one short pattern, and JavaScript checks it for you.

Regex looks scary at first, because it is dense. The secret is to build each pattern step by step, testing as you go. By the end of this lesson you will read and write patterns for all four KYC fields, and use them in a live form. This lesson builds on the JavaScript Strings lesson and pairs well with the JavaScript Form Validation lesson.

Creating a Regular Expression

The usual way to write a regex is a literal: the pattern between two forward slashes, like /chai/. Letters and digits in a pattern match themselves. The simplest method, pattern.test(text), returns true if the pattern is found anywhere in the text.

const pattern = /chai/;

console.log(pattern.test("One masala chai, please"));   // true
console.log(pattern.test("One cold coffee, please"));   // false
console.log(pattern.test("CHAI for table 4"));          // false (case matters)

// The i flag ignores upper and lower case
console.log(/chai/i.test("CHAI for table 4"));          // true

// The same pattern built from a string, useful when the text comes from a variable
const word = "dosa";
const fromString = new RegExp(word, "i");
console.log(fromString.test("Two Masala Dosa"));        // true

The letter after the closing slash is a flag. Flags change how the whole pattern behaves:

FlagMeaning
iIgnore case: A and a are treated the same
gGlobal: find all matches, not just the first (used with match, matchAll and replace)

Use new RegExp("...") only when the pattern comes from a variable. Literals are easier to read.

Character Classes and Quantifiers

A character class matches one character from a group. A quantifier says how many times the thing before it may repeat. These two ideas do most of the work in any pattern.

PatternMatches
\dOne digit, 0 to 9
\wOne letter, digit or underscore
\sOne space, tab or new line
[A-Z]One capital letter
[6-9]One digit from 6 to 9
[^0-9]One character that is not a digit (^ inside brackets means “not”)
.Any one character except a new line (write \. for a real dot)
QuantifierMeaning
{n}Exactly n times, so \d{6} is six digits
{n,m}Between n and m times
+One or more times
*Zero or more times
?Zero or one time (optional)
// \d means "any digit 0 to 9"
console.log(/\d/.test("Flat 4B"));         // true
console.log(/\d/.test("Flat B"));          // false

// [A-Z] means "any capital letter", [6-9] means "a digit from 6 to 9"
console.log(/[A-Z]/.test("pune"));         // false
console.log(/[6-9]/.test("5 and 7"));      // true

// \w is a letter, digit or underscore; \s is a space, tab or new line
console.log(/\w\s\w/.test("MG Road"));     // true (G, space, R)

// Quantifiers say how many times
console.log(/\d{3}/.test("Room 12"));      // false (needs 3 digits in a row)
console.log(/\d{3}/.test("Room 120"));     // true
console.log(/^\d+$/.test("400001"));       // true  (+ means one or more)
console.log(/^\d*$/.test(""));             // true  (* means zero or more)
console.log(/^colou?r$/.test("color"));    // true  (? means optional)

Anchors and Building a Pattern Step by Step

By default, test looks for the pattern anywhere in the text. For validation that is wrong: “PIN 560001” contains six digits, but it is not a PIN code. Anchors fix this. ^ means “the start of the text” and $ means “the end”. With both, the whole text must match the pattern, and nothing else is allowed.

Watch the PIN code pattern grow in three steps. Each line of output shows a sample, then the result of step 1, step 2 and step 3:

// Goal: a 6-digit PIN code that does not start with 0

const step1 = /\d{6}/;            // six digits somewhere
const step2 = /[1-9]\d{5}/;       // first digit 1 to 9, then five more digits
const pinCode = /^[1-9]\d{5}$/;   // ^ start and $ end: nothing else allowed

const samples = ["110001", "012345", "56001", "4000012", "PIN 560001"];

samples.forEach((s) => {
  console.log(s, step1.test(s), step2.test(s), pinCode.test(s));
});

// 110001 true true true
// 012345 true false false
// 56001 false false false
// 4000012 true true false
// PIN 560001 true true false

Step 1 accepts “012345”, which is wrong. Step 2 rejects it by asking for the first digit in [1-9], then five more, written \d{5}. But step 2 still accepts seven digits and extra text. Step 3 adds the anchors and gets every case right. The same method gives the other KYC patterns:

FieldPatternRead it as
PIN code/^[1-9]\d{5}$/A digit 1 to 9, then 5 digits
Mobile/^[6-9]\d{9}$/A digit 6 to 9, then 9 digits
PAN/^[A-Z]{5}\d{4}[A-Z]$/5 capitals, 4 digits, 1 capital
Vehicle/^[A-Z]{2}\d{2}[A-Z]{1,2}\d{4}$/State, district, 1 or 2 series letters, 4 digits
A regex checks the format, not the truth. ABCDE1234F has a valid PAN format, but only the tax department can say whether it is a real PAN. Always check real data on the server too.

Finding Matches: match and matchAll

To extract text instead of just checking it, use string methods:

  • text.match(pattern) without g returns the first match with details such as its position, or null.
  • text.match(pattern) with g returns an array of every matching string, or null.
  • text.matchAll(pattern) needs g and gives every match with its groups, ready for a for...of loop.

Round brackets in a pattern create a group, which captures that part of the match separately as m[1], m[2] and so on.

const address = "Ship to 411014, Pune. Old address: 560034, Bengaluru. Office: 600028.";

// match without g: the first match, with details
const first = address.match(/[1-9]\d{5}/);
console.log(first[0], "found at position", first.index);   // 411014 found at position 8

// match with g: an array of every match
console.log(address.match(/[1-9]\d{5}/g));   // ["411014", "560034", "600028"]

// No match gives null, not an empty array
console.log("No PIN here".match(/[1-9]\d{5}/g));   // null

// matchAll with g: every match with its groups (here, the city after the PIN)
const withCity = /([1-9]\d{5}), (\w+)/g;
for (const m of address.matchAll(withCity)) {
  console.log(m[2] + ": " + m[1]);
}
// Pune: 411014
// Bengaluru: 560034

The office PIN 600028 is in the g match list but not in the matchAll loop, because that pattern also needs a comma and a city name after the PIN.

Replacing Text with replace

text.replace(pattern, replacement) returns a new string. Without g, only the first match is replaced. Inside the replacement, $1 and $2 insert the groups. You can also pass a function that receives each match and returns its replacement. This is very useful for cleaning what people type before you validate it:

// Clean what the user typed: remove all spaces, make it upper case
const typedPan = " abcde 1234 f ";
const pan = typedPan.replace(/\s+/g, "").toUpperCase();
console.log(pan);   // ABCDE1234F

// Without g, only the first match is replaced
console.log("1-2-3".replace(/-/, "+"));    // 1+2-3
console.log("1-2-3".replace(/-/g, "+"));   // 1+2+3

// Use groups in the replacement with $1, $2 ...: hide the middle of a mobile number
const mobile = "9000012345";   // a made-up number
console.log(mobile.replace(/^(\d{2})\d{6}(\d{2})$/, "$1******$2"));   // 90******45

// Or pass a function to build the replacement
const price = "Total: 125000 rupees";
console.log(price.replace(/\d+/, (n) => Number(n).toLocaleString("en-IN")));
// Total: 1,25,000 rupees

Named Groups

Numbered groups like m[3] are hard to read. A named group, written (?<name>...), lets you read the part as match.groups.name. Here the vehicle number is split into its state code, district number, series and number:

const vehicle = /^(?<state>[A-Z]{2})(?<district>\d{2})(?<series>[A-Z]{1,2})(?<number>\d{4})$/;

const result = "MH12AB1234".match(vehicle);
console.log(result.groups.state);      // MH
console.log(result.groups.district);   // 12
console.log(result.groups.series);     // AB
console.log(result.groups.number);     // 1234

// Destructuring makes it neat
const { state, number } = "KA05M9876".match(vehicle).groups;
console.log(state, number);            // KA 9876

console.log(vehicle.test("MH12ABC1234"));   // false (3 series letters)
console.log("mh12ab1234".match(vehicle));   // null (lower case)

Real-Life Example: A Live KYC Form

Now let us use all four patterns in a form. Each field is checked as you type. The clean function first removes spaces and hyphens and makes letters upper case, so “mh 12 ab 1234” is accepted. The vehicle field uses named groups to show the state and district.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>KYC Form</title>
  <style>
    body { font-family: sans-serif; padding: 16px; max-width: 420px; }
    label { display: block; margin-top: 12px; font-weight: bold; }
    input { width: 100%; padding: 8px; font-size: 16px; box-sizing: border-box; }
    .hint { font-size: 14px; margin: 4px 0 0; }
    .ok { color: green; }
    .bad { color: crimson; }
  </style>
</head>
<body>
  <h2>Complete Your KYC</h2>
  <form id="kyc" novalidate>
    <label for="pin">PIN code</label>
    <input id="pin" inputmode="numeric" placeholder="560001">
    <p class="hint" id="pin-hint"></p>

    <label for="mobile">Mobile number</label>
    <input id="mobile" inputmode="numeric" placeholder="10 digits">
    <p class="hint" id="mobile-hint"></p>

    <label for="pan">PAN</label>
    <input id="pan" placeholder="ABCDE1234F">
    <p class="hint" id="pan-hint"></p>

    <label for="vehicle">Vehicle number (optional)</label>
    <input id="vehicle" placeholder="MH12AB1234">
    <p class="hint" id="vehicle-hint"></p>

    <p><button type="submit">Submit</button></p>
    <p id="result"></p>
  </form>

  <script>
    const rules = {
      pin: { pattern: /^[1-9]\d{5}$/, message: "6 digits, not starting with 0" },
      mobile: { pattern: /^[6-9]\d{9}$/, message: "10 digits, starting with 6, 7, 8 or 9" },
      pan: { pattern: /^[A-Z]{5}\d{4}[A-Z]$/, message: "5 letters, 4 digits, 1 letter" },
      vehicle: {
        pattern: /^(?<state>[A-Z]{2})(?<district>\d{2})(?<series>[A-Z]{1,2})(?<number>\d{4})$/,
        message: "Like MH12AB1234",
        optional: true,
      },
    };

    function clean(value) {
      return value.replace(/[\s-]+/g, "").toUpperCase();   // allow spaces and hyphens
    }

    function check(id) {
      const input = document.getElementById(id);
      const hint = document.getElementById(id + "-hint");
      const rule = rules[id];
      const value = clean(input.value);

      if (value === "" && rule.optional) {
        hint.textContent = "";
        return true;
      }
      const match = value.match(rule.pattern);
      if (match) {
        hint.className = "hint ok";
        hint.textContent = match.groups
          ? "Valid: registered in state " + match.groups.state + ", district " + match.groups.district
          : "Looks good";
        return true;
      }
      hint.className = "hint bad";
      hint.textContent = rule.message;
      return false;
    }

    Object.keys(rules).forEach((id) => {
      document.getElementById(id).addEventListener("input", () => check(id));
    });

    document.querySelector("#kyc").addEventListener("submit", (event) => {
      event.preventDefault();
      const results = Object.keys(rules).map(check);
      const allOk = results.every(Boolean);
      document.querySelector("#result").textContent = allOk
        ? "All details are valid. KYC submitted."
        : "Please fix the fields marked in red.";
    });
  </script>
</body>
</html>

Try these changes:

  1. Type a PIN code starting with 0 and watch the message. Then type a correct one.
  2. Change the mobile rule so that an optional +91 at the start is allowed. Hint: (\+91)?, where \+ means a real plus sign.
  3. Add an email field with a simple pattern such as /^[^\s@]+@[^\s@]+\.[^\s@]+$/ and test it with student@example.com.

Common Mistakes

  • Forgetting the anchors. Without ^ and $, /[6-9]\d{9}/ also accepts a 15-digit number, because it finds 10 good digits inside it.
  • Using the g flag with test. A regex with g remembers where its last match ended (in lastIndex), so calling test again on the same text can give a different answer:
const pinWithG = /^[1-9]\d{5}$/g;   // g is not needed here

console.log(pinWithG.test("560001"));   // true
console.log(pinWithG.test("560001"));   // false  (!)
console.log(pinWithG.test("560001"));   // true

// Fix: no g flag when you only want test()
const pin = /^[1-9]\d{5}$/;
console.log(pin.test("560001"), pin.test("560001"));   // true true
  • Forgetting to escape special characters. ., +, ?, *, ( and $ have special meanings. Put a backslash before them to match the real character, as in \. for a dot.
  • Being stricter than people type. Users add spaces, hyphens and small letters. Clean the input with replace and toUpperCase first, then validate.
  • Writing one giant pattern at once. Build it in small steps and test each step with good and bad samples.

Practice Task: IFSC Code and Date of Birth

Two more fields appear in banking forms. Write a pattern for each one:

  • An IFSC code (a bank branch code): 4 capital letters, then the digit 0, then 6 capital letters or digits. Example: ABCD0123456.
  • A date of birth in the form DD-MM-YYYY, with named groups day, month and year. The day must be 01 to 31, the month 01 to 12, and the year must start with 19 or 20. Hint: use |, which means “or”, inside a group.
  • Run the samples and make sure every expected result appears.
// TODO 1: an IFSC code pattern.
//   4 capital letters, then the digit 0, then 6 capital letters or digits.
const ifsc = /TODO/;

// TODO 2: a date of birth pattern DD-MM-YYYY with named groups day, month, year.
//   Day 01 to 31, month 01 to 12, year 19xx or 20xx.
const dob = /TODO/;

const ifscSamples = ["ABCD0123456", "abcd0123456", "ABCD1123456", "ABCD012345", "XYZW0A1B2C3"];
ifscSamples.forEach((s) => console.log(s, ifsc.test(s)));
// Expected: true, false, false, false, true

const dobSamples = ["15-08-2004", "31-12-1999", "32-01-2000", "10-13-2001", "5-6-2002"];
dobSamples.forEach((s) => {
  const m = s.match(dob);
  console.log(s, m ? "year " + m.groups.year : "invalid");
});
// Expected: year 2004, year 1999, invalid, invalid, invalid
Show one possible answer (try it yourself first!)
const ifsc = /^[A-Z]{4}0[A-Z0-9]{6}$/;

const dob = /^(?<day>0[1-9]|[12]\d|3[01])-(?<month>0[1-9]|1[0-2])-(?<year>(19|20)\d{2})$/;

const ifscSamples = ["ABCD0123456", "abcd0123456", "ABCD1123456", "ABCD012345", "XYZW0A1B2C3"];
ifscSamples.forEach((s) => console.log(s, ifsc.test(s)));
// ABCD0123456 true
// abcd0123456 false
// ABCD1123456 false
// ABCD012345 false
// XYZW0A1B2C3 true

const dobSamples = ["15-08-2004", "31-12-1999", "32-01-2000", "10-13-2001", "5-6-2002"];
dobSamples.forEach((s) => {
  const m = s.match(dob);
  console.log(s, m ? "year " + m.groups.year : "invalid");
});
// 15-08-2004 year 2004
// 31-12-1999 year 1999
// 32-01-2000 invalid
// 10-13-2001 invalid
// 5-6-2002 invalid

Key Takeaways

  • A regular expression is a pattern for text, written between slashes, like /^[1-9]\d{5}$/.
  • Character classes (\d, \w, \s, [A-Z]) match one character; quantifiers ({n}, +, *, ?) say how many.
  • Use ^ and $ to make the whole text match, which is what validation needs.
  • test checks, match and matchAll extract, and replace changes text. The g flag means all matches; i ignores case.
  • Groups capture parts of a match; named groups make them readable as match.groups.name.
  • Build patterns step by step, clean the input first, and remember that a regex checks only the format.

❓ Frequently Asked Questions

A regex can only check that the format is right. To know that a number or PAN really exists and belongs to the user, you need a server-side check, such as an OTP for a mobile number.
Both use the same kind of pattern. The pattern attribute is quick for simple forms, while JavaScript lets you clean the input, show custom messages and use named groups. Many forms use both.
Regex has several flavours, and some features differ between languages. Make sure your tester is set to JavaScript, and remember that in a string passed to new RegExp you must write each backslash twice.
For short inputs like form fields they are very fast. Badly written patterns with many nested repeats can become slow on long text, so keep patterns simple and anchored.
Was this lesson helpful?

🎯 Finished this lesson?

🔗 Related Tutorials
🤖 Got a Doubt? Ask AI
Get instant answers in Hindi or English
🤖
Z2T Bot
Online
Ask me anything about this tutorial — I'm here! 👋