magic-regexp vs regexp-tree vs safe-regex: Regular Expression Utilities in JavaScript (2026)
TL;DR
magic-regexp is the UnJS composable regex builder — write regex with TypeScript-first chainable API, get compile-time type safety for capture groups, no regex syntax needed. regexp-tree is the regex AST toolkit — parse, transform, optimize, and generate regular expressions using an abstract syntax tree. safe-regex detects vulnerable regex patterns — prevents ReDoS (Regular Expression Denial of Service) attacks by detecting exponential backtracking. In 2026: magic-regexp for building readable regex, regexp-tree for regex tooling, safe-regex for security auditing.
Key Takeaways
- magic-regexp: ~500K weekly downloads — UnJS, composable regex, typed capture groups
- regexp-tree: ~15M weekly downloads — regex parser/transformer/optimizer, used by ESLint
- safe-regex: ~10M weekly downloads — ReDoS detection, prevents catastrophic backtracking
- Completely different tools: build regex, transform regex, secure regex
- magic-regexp makes regex readable and type-safe
- safe-regex is critical for validating user-supplied patterns
magic-regexp
magic-regexp — composable regex:
Building regex
import { createRegExp, exactly, oneOrMore, digit, letter, anyOf } from "magic-regexp"
// Instead of: /^\d{3}-\d{3}-\d{4}$/
const phoneRegex = createRegExp(
exactly(digit.times(3))
.and("-")
.and(digit.times(3))
.and("-")
.and(digit.times(4)),
["g"]
)
// Instead of: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
const emailRegex = createRegExp(
oneOrMore(anyOf(letter, digit, exactly("._%+-")))
.and("@")
.and(oneOrMore(anyOf(letter, digit, exactly(".-"))))
.and(".")
.and(letter.times.atLeast(2))
)
Named capture groups (type-safe)
import { createRegExp, exactly, oneOrMore, digit, anyOf } from "magic-regexp"
// Create regex with named groups:
const dateRegex = createRegExp(
exactly(digit.times(4)).as("year")
.and("-")
.and(digit.times(2)).as("month")
.and("-")
.and(digit.times(2)).as("day")
)
// TypeScript knows the capture groups:
const match = "2026-03-09".match(dateRegex)
if (match) {
match.groups.year // string — TypeScript infers this
match.groups.month // string
match.groups.day // string
}
Common patterns
import {
createRegExp, exactly, maybe, oneOrMore, digit, word,
charIn, charNotIn, anyOf, linefeed, tab,
} from "magic-regexp"
// URL slug: /^[a-z0-9]+(-[a-z0-9]+)*$/
const slugRegex = createRegExp(
oneOrMore(charIn("a-z0-9"))
.and(
exactly("-").and(oneOrMore(charIn("a-z0-9"))).times.any()
)
)
// Semver: /^(\d+)\.(\d+)\.(\d+)(-[\w.]+)?$/
const semverRegex = createRegExp(
oneOrMore(digit).as("major")
.and(".")
.and(oneOrMore(digit)).as("minor")
.and(".")
.and(oneOrMore(digit)).as("patch")
.and(maybe(exactly("-").and(oneOrMore(anyOf(word, exactly("."))))))
)
const match = "1.2.3-beta.1".match(semverRegex)
// match.groups.major → "1"
// match.groups.minor → "2"
// match.groups.patch → "3"
Why it matters
Traditional regex:
/^(?:https?:\/\/)?(?:www\.)?([a-zA-Z0-9-]+(?:\.[a-zA-Z]{2,})+)(?:\/.*)?$/
magic-regexp:
createRegExp(
maybe(anyOf("http://", "https://"))
.and(maybe("www."))
.and(oneOrMore(anyOf(letter, digit, exactly("-")))
.and(oneOrMore(exactly(".").and(letter.times.atLeast(2)))))
.as("domain")
.and(maybe(exactly("/").and(oneOrMore(charNotIn(" ")))))
)
Benefits:
✅ Readable — no cryptic symbols
✅ Type-safe — capture groups are typed
✅ Composable — build from smaller parts
✅ Maintainable — easy to modify
regexp-tree
regexp-tree — regex AST toolkit:
Parse regex
import regexpTree from "regexp-tree"
// Parse a regex into an AST:
const ast = regexpTree.parse("/^[a-z]+\\d{2,4}$/i")
console.log(JSON.stringify(ast, null, 2))
// → {
// type: "RegExp",
// body: {
// type: "Alternative",
// expressions: [
// { type: "Assertion", kind: "^" },
// { type: "Repetition", expression: { type: "CharacterClass", ... }, quantifier: { type: "+", ... } },
// { type: "Repetition", expression: { type: "Char", value: "\\d" }, quantifier: { type: "{2,4}", ... } },
// { type: "Assertion", kind: "$" },
// ],
// },
// flags: "i",
// }
Optimize regex
import regexpTree from "regexp-tree"
// Optimize a regex:
const optimized = regexpTree.optimize("/[a-zA-Z0-9_]|[0-9]/")
console.log(optimized.toString())
// → /[\w]/ (simplified character class)
regexpTree.optimize("/(?:a|b|c)/").toString()
// → /[abc]/
regexpTree.optimize("/(a)(b)\\1\\2/").toString()
// → /(a)(b)\1\2/ (already optimal)
Transform regex
import regexpTree from "regexp-tree"
// Transform: convert named groups to numbered groups
const result = regexpTree.transform("/(?<year>\\d{4})-(?<month>\\d{2})/", {
NamedGroup(path) {
// Replace named groups with regular groups:
path.replace({
type: "Group",
capturing: true,
expression: path.node.expression,
})
},
})
console.log(result.toString())
// → /(\\d{4})-(\\d{2})/
Generate regex
import regexpTree from "regexp-tree"
// Build regex from AST:
const ast = {
type: "RegExp",
body: {
type: "Alternative",
expressions: [
{ type: "Char", value: "a" },
{ type: "Repetition",
expression: { type: "Char", value: "b" },
quantifier: { type: "+", greedy: true },
},
],
},
flags: "g",
}
const regex = regexpTree.generate(ast)
// → /ab+/g
How ESLint uses regexp-tree
// eslint-plugin-regexp uses regexp-tree internally:
// It parses regex literals in your code,
// analyzes them for issues, and suggests fixes:
// Before: /[0-9]/
// Suggestion: Use \d instead → /\d/
// Before: /[a-zA-Z0-9_]/
// Suggestion: Use \w instead → /\w/
// Before: /a{1}/
// Suggestion: Unnecessary quantifier → /a/
safe-regex
safe-regex — ReDoS detection:
Basic usage
import safe from "safe-regex"
// Check if a regex is safe from ReDoS:
safe(/^(a+)+$/) // false — exponential backtracking!
safe(/^([a-zA-Z0-9]+)$/) // true — safe
safe(/^(a|b|c)+$/) // true — safe
safe(/(a+){10}/) // false — nested quantifiers
safe(/\d+/) // true — safe
// With string:
safe("^(a+)+$") // false
Why ReDoS matters
// ReDoS — Regular Expression Denial of Service:
// Vulnerable regex:
const emailRegex = /^([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+)\.([a-zA-Z]{2,})$/
// Malicious input that causes exponential backtracking:
const evil = "a".repeat(30) + "@" + "b".repeat(30) + "!"
// This could take MINUTES to process with a vulnerable regex
// The pattern /^(a+)+$/ with input "aaaaaaaaaaaaaaaaX":
// → Tries every possible way to divide "a"s between groups
// → 2^n combinations — exponential time
// → With 30 "a"s, could take hours
Validating user input patterns
import safe from "safe-regex"
// In a search API that accepts regex:
app.get("/api/search", (req, res) => {
const { pattern } = req.query
if (!safe(pattern)) {
return res.status(400).json({
error: "Regex pattern is potentially unsafe (ReDoS risk)",
})
}
const regex = new RegExp(pattern, "i")
const results = packages.filter((pkg) => regex.test(pkg.name))
res.json(results)
})
Configuration
import safe from "safe-regex"
// Adjust the star height limit:
safe(/^(a+)+$/, { limit: 25 }) // Default limit is 25
// Star height = nested quantifier depth
// Higher limit = more permissive but less safe
Common vulnerable patterns
Vulnerable (exponential backtracking):
/^(a+)+$/ — nested quantifiers
/^(a|aa)+$/ — overlapping alternation
/^([a-zA-Z]+)*$/ — quantified group with quantified content
/^(a+b?)*$/ — optional element in quantified group
/(.*a){x}/ — .* before specific char in group
Safe alternatives:
/^a+$/ — single quantifier (no nesting)
/^[a-zA-Z]+$/ — character class (no group needed)
/^(?:a|b)+$/ — non-overlapping alternation
/^\w+@\w+\.\w+$/ — simple character classes
Feature Comparison
| Feature | magic-regexp | regexp-tree | safe-regex |
|---|---|---|---|
| Purpose | Build regex | Parse/transform regex | Detect unsafe regex |
| API | Composable builder | AST manipulation | Safety checker |
| TypeScript types | ✅ (capture groups) | ✅ | ✅ |
| Parse regex | ❌ | ✅ | ❌ |
| Build regex | ✅ | ✅ (from AST) | ❌ |
| Optimize regex | ❌ | ✅ | ❌ |
| Transform regex | ❌ | ✅ | ❌ |
| ReDoS detection | ❌ | ❌ | ✅ |
| Named groups | ✅ (type-safe) | ✅ (AST) | N/A |
| Used by | UnJS projects | ESLint, Babel | Security tools |
| Weekly downloads | ~500K | ~15M | ~10M |
When to Use Each
Use magic-regexp if:
- Want readable, composable regex without cryptic syntax
- Need type-safe capture groups in TypeScript
- Building regex that will be maintained by a team
- In the UnJS ecosystem
Use regexp-tree if:
- Building regex tooling (linters, codemods, analyzers)
- Need to parse, optimize, or transform regex programmatically
- Building an ESLint plugin that works with regex
- Need AST-level regex manipulation
Use safe-regex if:
- Accepting user-supplied regex patterns
- Auditing your codebase for ReDoS vulnerabilities
- Building a WAF or input validation layer
- Need to prevent catastrophic backtracking
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on magic-regexp v0.8.x, regexp-tree v0.1.x, and safe-regex v2.x.