Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Node.js/Express/TypeScript-specific code review overlay. Extends the universal code-reviewer skill with Node.js version-aware rules. Trigger when reviewing Express route handlers, middleware, REST controllers, GraphQL resolvers, Apollo Server setup, JWT/OAuth middleware, Bull/BullMQ job processors, or any .ts/.js file in a Node.js backend project. Keywords: Express, REST API, GraphQL, Apollo, JWT, OAuth, BullMQ, Bull, queue, middleware, TypeScript strict, tsconfig, req, res, next, router. Do NOT
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 129% | 0% |
| case-08 | ✓→✓ | = Same ✓ | 127% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 230% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 365% | 0% |
| case-07 | ✓→✓ | = Same ✓ | 164% | 0% |
This skill extends code-reviewer (the universal skill). Always apply the universal skill's full checklist first, then apply the Node.js-specific rules in this file on top.
Composition order:
code-reviewer (universal pillars: correctness, security, performance, DRY, tests, docs)Run these commands before touching any code. Version determines which rules apply.
bash# Node.js runtime version node --version cat .nvmrc 2>/dev/null cat .node-version 2>/dev/null # Package manager and key dependencies cat package.json | grep -E '"(node|engines|typescript|express|graphql|apollo-server|bullmq|bull|jsonwebtoken|zod|joi|helmet|express-rate-limit)"' # TypeScript config cat tsconfig.json 2>/dev/null | grep -E '"(strict|target|module|moduleResolution)"' # Check if ESM or CJS cat package.json | grep '"type"'
Report at the top of your review:
🔍 Environment: Node.js vX.Y | TypeScript X.Y | Express X.Y | ESM/CJS
Packages: [graphql, apollo-server, bullmq/bull, jsonwebtoken, zod/joi — versions]Then apply the version-specific rules below that match.
Dockerfile, .nvmrc, engines in package.json) targeting Node 18.fs/url/crypto behaviors removed in 20+; flag url.parse() → use new URL().require() of ESM modules still requires workaround — flag ugly createRequire hacks; they'll be fixed on Node 22.url.parse() still works but flag it: use new URL() instead.fetch is stable — flag node-fetch or axios used only for simple GET/POST where native fetch suffices.fetch is fully stable — flag node-fetch package as unnecessary for simple requests.require(ESM) works from 22.12+ without flags — flag createRequire workarounds.url.parse() emits DEP0169 warning — flag any usage; replace with new URL().--experimental-permission model redesigned — flag old permission flags if present.strict: true in tsconfig.json — flag if missing. In a strict project, also flag:any types on request handlers, resolver args, or job payloads — replace with typed interfaces.as any casts — flag each one; they hide bugs.!) without a guard — flag unsafe use.req.body, req.params, req.query — must be typed:ts // ❌ Untyped const { email } = req.body;
// ✅ Typed with Zod or interface const { email } = CreateUserSchema.parse(req.body);
"moduleResolution": "bundler" or "node16" — flag if using "node" (legacy) in a new project with ESM."target" vs runtime — flag target: "es5" on Node 20/22; use "es2022" or higher.@types/* packages for dependencies that have them available.tsconfig.json missing "paths" configuration causing deep relative imports (../../../../utils).helmet, cors, rate limiting, auth) placed after route definitions.ts // ❌ Helmet applied after routes — security headers missing on those routes app.get('/users', usersRouter); app.use(helmet());
// ✅ Helmet first app.use(helmet()); app.get('/users', usersRouter);
express.json() or express.urlencoded() body parsers when handlers access req.body.body-parser package usage — it's bundled in Express 4.16+ as express.json().app.use((err, req, res, next) => {...})) — unhandled errors leak stack traces.next() called after res.send() / res.json() — double response bug.return before res.send() in conditional branches — causes "headers already sent" errors.helmet() — flag any Express app not using helmet middleware. Helmet sets: X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security, X-XSS-Protection, and more.cors({ origin: '*' }) in production; must whitelist specific origins./login, /register, /reset-password) missing express-rate-limit or equivalent.req.body, req.query, req.params used without validation. Must use Zod, Joi, or equivalent before entering business logic:ts // ❌ Raw body access — no validation const user = await createUser(req.body.email, req.body.password);
// ✅ Validated first const { email, password } = CreateUserSchema.parse(req.body); const user = await createUser(email, password);
hpp middleware if query params are used for filtering.express.static — flag serving directories with sensitive files; check path traversal risk.JWT_SECRET, API keys, or DB credentials; must use process.env with validation at startup.fs.readFileSync, JSON.parse on large payloads, heavy CPU loops. Use async fs.promises.* or offload to worker threads.limit/offset or cursor).async route handlers without try/catch or an async error wrapper:ts // ❌ Unhandled promise rejection crashes Express app.get('/users', async (req, res) => { const users = await getUsers(); // if this throws, Express doesn't catch it res.json(users); });
// ✅ Wrapped app.get('/users', asyncHandler(async (req, res) => { const users = await getUsers(); res.json(users); }));
compression middleware — flag large JSON API responses without gzip compression.JWT_SECRET that is short, hardcoded, or a simple word. Must be a long random string from environment.{ algorithm: 'none' } (critical). Flag HS256 for multi-service architectures; prefer RS256/ES256 (asymmetric).jwt.verify() calls that don't check iss, aud, exp:ts // ❌ No audience/issuer check const decoded = jwt.verify(token, secret);
// ✅ Full claims validation const decoded = jwt.verify(token, secret, { algorithms: 'RS256'], audience: process.env.JWT_AUDIENCE, issuer: process.env.JWT_ISSUER, });
localStorage for JWTs; use httpOnly cookies.expiresIn: '30d'); access tokens should be short-lived (15min–1h); use refresh tokens for longevity.jsonwebtoken vs jose — flag jsonwebtoken in Node 22 projects for new code; jose is more modern and supports Web Crypto API natively.state parameter — flag OAuth flows missing CSRF state validation.introspection: true (or omitted, as it defaults to true in dev) in production Apollo Server config. Should be process.env.NODE_ENV !== 'production'.depthLimit plugin — unbounded nested queries are a DoS vector.costLimit or complexityLimit plugin for production APIs.ts // ❌ N+1 — one DB call per user in the list posts: async (user) => await Post.findAll({ where: { userId: user.id } }),
// ✅ DataLoader batches into one query posts: async (user, _, { loaders }) => loaders.postsByUserId.load(user.id),
args: any on resolvers; use generated types from graphql-codegen.formatError not configured; Apollo default may expose stack traces in production.args directly without Zod/Joi validation.onConnect.attempts + backoff — flag jobs with no retry strategy:ts // ❌ No retry — job silently disappears on failure queue.add('send-email', { to: email });
// ✅ With retry and exponential backoff queue.add('send-email', { to: email }, { attempts: 3, backoff: { type: 'exponential', delay: 2000 }, });
failed event listener or dead letter queue strategy.ts // ❌ New connection per queue const emailQueue = new Queue('email', { connection: { host, port } }); const smsQueue = new Queue('sms', { connection: { host, port } });
// ✅ Shared connection const connection = new IORedis({ host, port, maxRetriesPerRequest: null }); const emailQueue = new Queue('email', { connection }); const smsQueue = new Queue('sms', { connection });
stalledInterval config for long-running jobs.bull (legacy) in new Node 22 projects; prefer bullmq (active development, better TypeScript support).GET endpoints with side effects, POST used for reads, DELETE with a body.200 for created resources (should be 201), 200 for empty results (should be 204), 500 for client errors (should be 4xx).ts { error: { code: 'VALIDATION_ERROR', message: '...', details: [...] } }
limit/offset or cursor params./v1/) in a project that has multiple consumers.Content-Type header — flag responses missing Content-Type: application/json when returning JSON.POST endpoints for financial or critical operations missing idempotency key support.supertest for HTTP integration tests on Express routes.afterAll / afterEach cleanup (open handles prevent Jest from exiting).jest.setTimeout set very high globally — symptom of slow or hanging tests.nock, msw, or jest.mock.Use the same format as code-reviewer (universal). Add a Node.js context line:
🔍 Environment: Node.js v22.x | TypeScript 5.x | Express 4.x | ESM
Packages: graphql 16.x, apollo-server 4.x, bullmq 5.x, jsonwebtoken 9.x, zod 3.x
## Code Review Summary
[... standard universal format ...]
### 🟢 Node.js-Specific Issues
[Issues found by this overlay, using the same severity/format as universal]async error handling is non-negotiable — unhandled promise rejections in Express crash the process silently in older setups and terminate in Node 15+. Always flag.helmet, CORS, rate limiting, and auth must come before route handlers. Flag any violation.security-auditor skill if available.Other measured skills in the registry, with their headline benchmark lift.