Regex patterns every developer should know

7 min read
regex

Most regular expressions people actually need in day-to-day work come from a small, reusable set. Memorising the syntax matters less than knowing which pattern to reach for.

Whitespace and trimming

^\s+|\s+$ matches leading and trailing whitespace, which is what String.prototype.trim() does internally. \s+ collapses runs of spaces, tabs and newlines into one match, useful when normalising pasted text before storage.

Emails: good enough, not perfect

A fully RFC 5322-compliant email regex is enormous and still wrong at the edges. For form validation, a pragmatic pattern works better paired with a confirmation email:

const emailLike = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

This rejects obviously malformed input without pretending to be a full validator. Never use regex alone to decide whether an email address can actually receive mail.

URLs and paths

^https?:\/\/[\w.-]+(?:\/[\w./?%&=-]*)?$ catches the common case of an http(s) URL with an optional path and query string. For anything more precise, parse with the URL API instead — a URL parser tool exists exactly because regex is the wrong tool for full RFC 3986 compliance.

Capturing groups vs non-capturing groups

Use (?:...) when you need to group alternatives but do not need the matched text back — it is faster and keeps your capture group indices predictable when the pattern grows.

const version = /^v(?:ersion)?[\s-]?(\d+)\.(\d+)\.(\d+)$/;

Here only the three numeric groups are captured; the optional "ersion" text is grouped but ignored.

Lookaheads for validation without consuming

Password strength rules are the classic use case:

  • (?=.*[a-z]) — at least one lowercase letter
  • (?=.*[A-Z]) — at least one uppercase letter
  • (?=.*\d) — at least one digit
  • (?=.*[^\w\s]) — at least one symbol

Chain lookaheads at the start of the pattern, then follow with the length requirement: ^(?=.[a-z])(?=.[A-Z])(?=.*\d).{8,}$.

Greedy vs lazy quantifiers

. is greedy and will match as much as possible before backtracking. .? is lazy and matches as little as possible. Extracting content between HTML-like tags is the classic trap:

"<b>one</b><b>two</b>".match(/<b>(.*)<\/b>/)[1];  // "one</b><b>two"
"<b>one</b><b>two</b>".match(/<b>(.*?)<\/b>/)[1]; // "one"

Escaping special characters

If you are building a pattern from user input, escape regex metacharacters first, or the input can change what the pattern matches entirely:

function escapeRegex(s) {
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

Catastrophic backtracking

Nested quantifiers like (a+)+ can take exponential time on pathological input — a classic ReDoS. Avoid nesting repetition, prefer atomic patterns, and test suspicious patterns against a long string of near-matches before shipping them anywhere user input reaches the pattern.

Test before you commit

Write the pattern, then run it against a batch of both matching and non-matching examples in a regex tester with live highlighting. Seeing which group matched what removes almost all of the guesswork, especially with lookaheads and nested groups.

Tools from this article

← All articles