The first regex I ever wrote was to find phone numbers in a customer service database. It took me forty minutes and looked like this: \+?\d{1,3}[-\s]?\(?\d{3}\)?[-\s]?\d{3,4}[-\s]?\d{4}. It worked, and I felt like a wizard for about six hours until I found a phone number it missed.
Regex has a reputation for being impossible. It isn't. It has a reputation for being ugly. That one is fair. But almost everything you'll ever need to do with it comes down to seven building blocks, and once you know them, the ugliness stops feeling like nonsense.
Building block one: literal characters
The simplest regex is just a word. cat matches 'cat'. It also matches 'cat' inside 'catalogue' and 'concatenate' — regex doesn't care about word boundaries unless you tell it to.
Most letters and digits are literal. A handful are special: . * + ? ( ) [ ] { } \ | ^ $ — these mean something else and have to be escaped with a backslash if you want them literally. So the regex to match a full stop is \. — not . (which matches any character, as we'll see next).
Building block two: 'any character' with .
A bare . matches any single character. c.t matches 'cat', 'cot', 'c9t', 'c!t' — anything with a c, one character, and a t. This is the first place people trip up: . is not a full stop, it's a wildcard.
If you want to match an actual full stop, escape it: \.. So the regex for a filename ending in .jpg is \.jpg, not .jpg.
Building block three: character sets [ ]
Square brackets let you list allowed characters. [aeiou] matches any single vowel. [0-9] matches any single digit. [a-zA-Z] matches any letter. You can combine: [a-zA-Z0-9_] is 'any letter, digit, or underscore' — this comes up so often that \w is shorthand for exactly that.
A caret inside brackets negates: [^0-9] means 'anything except a digit'. Handy for 'find the non-numeric parts of this string'.
Building block four: repetition * + ? { }
By default, one pattern matches one character. To match more, you add a quantifier immediately after: * means 'zero or more', + means 'one or more', ? means 'zero or one', {3} means 'exactly 3', {2,5} means 'between 2 and 5'.
So [0-9]+ matches one or more digits (any positive integer). [a-z]{3,10} matches a lowercase word between 3 and 10 letters. The famous .* is 'zero or more of anything' — powerful and slightly dangerous, because it's greedy (it grabs as much as it can). Add a ? after any quantifier to make it lazy: .*? grabs as little as it can.
Building block five: anchors ^ and $
^ means 'start of the string' and $ means 'end of the string'. Without them, your regex matches anywhere inside the input. ^cat matches 'cat' at the beginning of a line but not 'concatenate'. cat$ matches lines ending in 'cat'. ^cat$ matches only the string 'cat', nothing more, nothing less.
This is the single most useful concept for validation. ^[0-9]{5}$ is 'exactly five digits, nothing else' — a US ZIP code. Without the anchors, that regex would happily match the '12345' inside 'phone: 123456789'.
Building block six: alternation |
The pipe means 'or'. cat|dog matches either 'cat' or 'dog'. Wrap it in parentheses to control scope: I love (cats|dogs) matches 'I love cats' or 'I love dogs'.
Alternation is greedy left-to-right — the first option that matches wins. If two options overlap, put the more specific one first: (https?|http) would never match 'https' properly, because http matches first. Write (https|http) instead.
Building block seven: capturing groups ( )
Parentheses do two things at once: they group patterns for a quantifier, and they capture the matched text so you can refer back to it. (\d{3})-(\d{4}) on '555-1234' captures '555' as group 1 and '1234' as group 2. Most languages then let you access those with $1, $2 or \1, \2.
This is how you do find-and-replace with reordering: swap a name from 'John Smith' to 'Smith, John' with the regex (\w+) (\w+) and replacement $2, $1. It's the closest regex gets to feeling like magic.
Seven patterns you can literally copy
Email (good enough for 99% of cases): ^[^\s@]+@[^\s@]+\.[^\s@]+$
US phone with optional formatting: ^\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$
URL: ^https?://[^\s]+$
Hex colour: ^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$
IPv4 address: ^(\d{1,3}\.){3}\d{1,3}$ (this one's loose — it accepts 999.999.999.999. Real IP validation needs code, not regex.)
Whitespace-only string: ^\s*$
Anything that isn't alphanumeric (strip for slugs): [^a-zA-Z0-9]+
One rule that will save you hours
Test your regex against real inputs before you deploy it. Every regex you write is wrong the first time. Copy 20 real examples from your actual data, paste them into a tester, and watch the matches highlight — the ones that don't light up are the ones your regex misses, and the ones that light up wrongly are the false positives.
That's it. Seven building blocks, one testing rule. You'll never be a regex wizard, but you'll never need to be — 90% of what you write is one of the seven patterns above, and the other 10% is just those blocks stacked differently. The wizardry is knowing when regex isn't the right tool at all: for anything nested, structured, or context-sensitive (HTML, JSON, code), reach for a parser instead.



