skills/backend/express/5.x/SKILL.md
Expert agent for Express 5.x specific features and migration from Express 4. Covers path-to-regexp v8 strict syntax, automatic promise rejection handling, removed deprecated methods, req.host changes, Bun/Deno compatibility, and Express 4 to 5 migration. WHEN: "Express 5", "Express v5", "express@5", "path-to-regexp v8", "express 5 migration", "express 5 breaking changes", "express 5 async", "express 5 wildcard", "express upgrade", "express 4 to 5", "named wildcard", "express bun", "express deno".
npx skillsauth add chrishuffman5/domain-expert backend-express-5xInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are a specialist in Express 5.x, the major release of Express.js (stable late 2024). Express 5 targets Node.js 18+ and introduces breaking changes focused on stricter path syntax, automatic async error handling, and removal of deprecated APIs. It also brings first-class Bun and Deno compatibility.
Classify the request:
express/SKILL.md for cross-version topicsAnalyze -- Determine whether the issue is Express 5 specific or general Express architecture. Most middleware patterns, project structure, and testing approaches are version-independent.
Recommend -- Provide before/after code showing the Express 4 vs Express 5 form. Always explain the breaking change rationale.
Express 5 upgrades from path-to-regexp v0.x to v8. This is the most pervasive breaking change.
Wildcard routes must be named:
// Express 4
app.get('/users/*', handler); // bare wildcard
app.get('/files/(.*)', handler); // unnamed regex group
// Express 5
app.get('/users/:splat*', handler); // named wildcard
app.get('/files/:filepath(.*)', handler); // named capture group
Stricter character rules:
// Express 4 -- loose syntax accepted
app.get('/a(b)?c', handler); // inline regex
app.get('/items/{id}', handler); // literal braces
// Express 5 -- must use formal syntax
app.get('/a{b}c', handler); // braces delimit optional segments
app.get('/items/:id', handler); // standard param syntax
Path validation at startup:
Express 5 validates all route paths at registration time. Invalid patterns throw immediately on app.get(), app.post(), etc., rather than at request time. This surfaces errors earlier and more clearly.
Common patterns and their Express 5 equivalents:
| Express 4 Pattern | Express 5 Equivalent | Notes |
|---|---|---|
| * | :name* | Named wildcard required |
| (.*) | :name(.*) | Named capture group |
| /a(b)?c | /a{b}c or /a:opt?c | Braces for optional segments |
| :param? | :param? | Optional params still supported |
| /path/* | /path/:rest* | Catch-all suffix |
The most impactful improvement for async code. Rejected promises and thrown errors in route handlers are automatically forwarded to next(err).
// Express 4 -- required explicit try/catch or wrapper
app.get('/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) throw new NotFoundError('User');
res.json(user);
} catch (err) {
next(err); // manual forwarding
}
});
// Express 5 -- clean async handlers
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) throw new NotFoundError('User');
res.json(user); // rejections auto-forwarded to error handler
});
Implications:
express-async-errors package -- no longer neededasyncHandler wrapperstry/catch that only calls next(err)throw in non-async handlers also auto-forwarded// Sync throw also works in Express 5
app.get('/risky', (req, res) => {
throw new Error('Sync error'); // forwarded to error middleware
});
| Express 4 (deprecated) | Express 5 (correct) | Notes |
|---|---|---|
| app.del('/path', fn) | app.delete('/path', fn) | Method alias removed |
| res.send(status, body) | res.status(status).send(body) | Argument overload removed |
| res.json(status, body) | res.status(status).json(body) | Argument overload removed |
| res.sendfile(path) | res.sendFile(path) | Case-corrected method |
| req.param('name') | req.params.name / req.query.name / req.body.name | Multi-source lookup removed |
| res.redirect(url, status) | res.redirect(status, url) | Argument order flipped |
In Express 5, req.host preserves the port number if present in the Host header:
app.set('trust proxy', 1);
// Host header: example.com:3000
// Express 4: req.host = 'example.com:3000', req.hostname = 'example.com'
// Express 5: req.host = 'example.com:3000', req.hostname = 'example.com'
// Both versions: use req.hostname for host-without-port
Best practice: Always use req.hostname when you need the host without port. Use req.host only when you specifically need the port included.
Express 5 works with Bun and Deno without modification:
// Bun (bun run server.js)
import express from 'express';
const app = express();
app.get('/', (req, res) => res.send('Running on Bun'));
app.listen(3000);
// Deno (deno run --allow-net --allow-env server.ts)
import express from 'npm:express@5';
const app = express();
app.get('/', (req: any, res: any) => res.send('Running on Deno'));
app.listen(3000);
Compatibility relies on Express's use of Node.js built-in http module, which both runtimes implement.
Caveats:
bcrypt with C++ bindings) may not work on all runtimesnpm install express@5
Search for wildcard and regex patterns in routes:
# Find potential issues
grep -rn "app\.\(get\|post\|put\|delete\|use\|patch\)" src/ | grep -E "\*|(\.\*)"
Fix each pattern:
// Before
app.get('/api/*', handler);
app.get('/docs/(.*)', handler);
router.get('/files/*', serveFiles);
// After
app.get('/api/:path*', handler);
app.get('/docs/:path(.*)', handler);
router.get('/files/:filepath*', serveFiles);
# Find deprecated patterns
grep -rn "app\.del\b" src/
grep -rn "res\.send(" src/ | grep -E "res\.send\(\d"
grep -rn "res\.json(" src/ | grep -E "res\.json\(\d"
grep -rn "res\.sendfile\b" src/
grep -rn "req\.param\b" src/
grep -rn "res\.redirect(" src/ | grep -E "redirect\(['\"]"
Fix each occurrence:
// Before // After
app.del('/item', fn) => app.delete('/item', fn)
res.send(200, data) => res.status(200).send(data)
res.json(201, data) => res.status(201).json(data)
res.sendfile(p) => res.sendFile(p)
req.param('id') => req.params.id
res.redirect('/new', 301) => res.redirect(301, '/new')
# Find patterns to remove
grep -rn "express-async-errors" src/ package.json
grep -rn "asyncHandler" src/
// Before (Express 4)
import 'express-async-errors';
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get('/users', asyncHandler(async (req, res) => {
const users = await User.findAll();
res.json(users);
}));
// After (Express 5)
app.get('/users', async (req, res) => {
const users = await User.findAll();
res.json(users);
});
Remove express-async-errors from package.json and any import statements.
// Before (Express 4)
app.get('/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
res.json(user);
} catch (err) {
next(err);
}
});
// After (Express 5)
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
});
Keep try/catch only when you need to handle specific errors locally (e.g., retry logic, fallback values).
Express 5 validates paths at startup. Run the app and check for errors:
node server.js
# Any invalid path patterns will throw immediately
npm test
Focus on:
res.send(status, body) patterns remain)npm install express@5app.del() with app.delete()res.send(status, body) to res.status(status).send(body)res.json(status, body) to res.status(status).json(body)res.sendfile() to res.sendFile()req.param() with explicit source (req.params, req.query, req.body)res.redirect(url, status) to res.redirect(status, url)* to :name*express-async-errors packageasyncHandler wrapperstry/catch blocks that only call next(err)trust proxy setting still correctExpress 5 requires Node.js 18+. If upgrading from Express 4 on an older Node.js version, you must upgrade Node.js first.
| Express Version | Minimum Node.js | Recommended Node.js | |---|---|---| | Express 4.x | Node 10+ | Node 20 LTS | | Express 5.x | Node 18+ | Node 22 LTS |
For cross-version topics (middleware patterns, project structure, testing, deployment), use the parent agent:
../references/architecture.md -- Middleware internals, routing, request/response lifecycle../references/best-practices.md -- Project structure, security, testing, deployment../references/diagnostics.md -- Common errors, debugging, performancetools
kubectl command-line usage and scripting: kubeconfig and context management, output formats (jsonpath, custom-columns, go-template), all major verbs (get, describe, apply, delete, exec, logs, port-forward, rollout, scale, drain), workload resources, config/storage, networking, RBAC, node management, debugging (CrashLoopBackOff, ImagePullBackOff, OOMKilled), and scripting patterns (dry-run, diff, wait, jq, kustomize). WHEN: "kubectl", "k8s CLI", "kubeconfig", "namespace", "pod", "deployment", "service", "ingress", "configmap", "secret", "rollout", "scale", "drain", "taint", "kustomize". Do NOT use for cluster architecture, sizing, upgrades, or workload design decisions — that's the `kubernetes` skill in the `containers` plugin. This skill is command syntax and scripting kubectl against an existing cluster, not cluster ops.
tools
Bash 5.x shell scripting, Unix text processing, and command-line automation: variables, parameter expansion, quoting, control flow, functions, I/O redirection, error handling (set -euo pipefail, trap), and the Unix tool ecosystem (grep, sed, awk, jq, find, sort, uniq, cut, xargs). Covers process management, networking (curl, ssh, rsync, nc), file locking (flock), parallel execution, and production script patterns. WHEN: "Bash", "bash", "shell", "sh", ".sh", "shell script", "sed", "awk", "grep", "jq", "find", "xargs", "curl", "ssh", "rsync", "cron", "pipe", "redirect", "here-doc", "shebang", "POSIX", "set -euo pipefail", "trap".
tools
Azure CLI (az) command syntax and scripting: authentication (interactive, service principal, managed identity, SSO), output formats and JMESPath queries, resource groups, VMs, storage accounts/blobs, networking (VNets, NSGs, load balancers, DNS), Entra ID, AKS, App Service, Functions, databases (SQL, Cosmos DB, MySQL, PostgreSQL), Key Vault, Monitor/alerting, and infrastructure scripting patterns. WHEN: "az ", "Azure CLI", "az login", "az vm", "az aks", "az storage", "az keyvault", "az monitor", "az ad", "az group", "az network", "az webapp", "az functionapp", "az sql", "az cosmosdb", "JMESPath", "az account". Do NOT use for Azure architecture, landing zones, or multi-subscription strategy — that's the `cloud-platforms` plugin. This skill is about command syntax and scripting the CLI, not deciding what to provision.
tools
AWS CLI v2 command syntax and scripting: authentication (profiles, SSO, assume-role, instance profiles), output formats and JMESPath queries, pagination and waiters, IAM, S3, Lambda, RDS, CloudFormation, ECS, EKS, CloudWatch, SSM, Route 53, STS, and VPC networking. WHEN: "aws ", "AWS CLI", "aws ec2", "aws s3", "aws lambda", "aws iam", "aws cloudformation", "aws ssm", "aws ecs", "aws eks", "aws rds", "aws cloudwatch", "aws route53", "aws sts". Do NOT use for AWS architecture, service selection, multi-account strategy, or FinOps — that's the `cloud-platforms` plugin. This skill is about command syntax and scripting the CLI, not deciding what to provision.