Skip to content

Node.js recipes for AI coding agents are prompts and verification gates that make Claude Code, Codex, and Cursor produce production-ready Express 5 services in TypeScript: clean shutdown, validated input, refresh-token rotation, rate limits, Prisma transactions that hold under concurrency, BullMQ jobs, and redacted logs. Output is accepted on gate evidence, not line-by-line reading.

Your Express API passes every test the agent wrote, then the first deploy drops in-flight requests, two customers buy the last item in stock at the same moment, and a refresh token shows up in plain text in your log search. The code looked idiomatic. Each bug is a production detail that the happy-path tests never exercised.

This page is for developers who build Node.js backends with an agent and want to stop reviewing every middleware by eye. It puts the production details into the prompts, then makes the machine prove them.

What you’ll walk away with from these Node.js recipes

Section titled “What you’ll walk away with from these Node.js recipes”
  • An AGENTS.md rule block for TypeScript on Node.js that all three tools follow, and an npm run check gate the agent runs before it says “done”
  • Seven copy-paste prompts: an Express 5 skeleton with clean shutdown, validation and error mapping, refresh-token rotation, rate limiting, a Prisma order transaction, a BullMQ worker, and redacted structured logging
  • A verification table that tells you which gate catches which bug, so you read gate output instead of the diff
  • Recovery steps for the six ways agent-written Node.js most often breaks

Set up Node.js conventions and a gate the agent must pass

Section titled “Set up Node.js conventions and a gate the agent must pass”

Two artifacts do most of the work: a rules file the agent reads on every task, and one command that fails when the code is not ready. Write both before you ask for any code.

  1. Pin a supported runtime. On the Node.js release schedule, Node.js 24 is Active LTS until 2026-10-20, when it enters Maintenance LTS, Node.js 22 is Maintenance LTS, and Node.js 20 reached end of life on 2026-04-30. Node.js 26 is scheduled to become LTS on 2026-10-28. Pin 24 in .nvmrc and in package.json:

    {
    "type": "module",
    "engines": { "node": ">=24" }
    }

    An explicit version stops the agent from reaching for workarounds that current Node.js no longer needs, such as dotenv where node --env-file=.env works.

  2. Add the gate to package.json. It is the definition of done for every prompt on this page.

    {
    "scripts": {
    "typecheck": "tsc --noEmit",
    "lint": "eslint .",
    "test": "vitest run",
    "check": "npm run typecheck && npm run lint && npm run test && npm audit --omit=dev --audit-level=high"
    }
    }

    Current majors on npm on 2026-09-26: express 5.2.1, zod 4.6.5, vitest 5.0.2, supertest 7.3.0, pino 10.3.1, bullmq 6.3.9, @prisma/client 7.10.0, testcontainers 12.1.0. Two packages need care, and the failure-modes section explains why: install typescript@~6.0.3 rather than latest if you use typescript-eslint type-aware rules, and install prisma and @prisma/client at the same version.

  3. Put the Node.js rules in AGENTS.md at the repository root. Codex reads AGENTS.md natively; the tabs below show how Claude Code loads it and how Cursor gets a copy.

    ## Node.js conventions
    - Node.js 24, ESM, TypeScript strict. Express 5, never Express 4 idioms.
    - Express 5 forwards rejected promises from handlers to the error middleware.
    Do not add express-async-errors or a catchAsync wrapper.
    - Named wildcards only: app.all('/*splat', ...), never app.get('*', ...).
    - Config comes from one Zod-validated module, src/config.ts. No process.env
    reads anywhere else.
    - One PrismaClient for the process, created in src/db.ts. Never per request.
    - Every error response uses { error: { code, message } }; no stack traces
    outside development.
    - Tests: Vitest and supertest. Database tests use Testcontainers, not mocks.
    - Never edit an existing test assertion, add .skip, or add .only to make a
    test pass. If a test looks wrong, stop and say why.
    - Done means `npm run check` passes. Paste its last 20 lines in your final message.

The last two rules matter most. The first stops the agent from weakening your tests to reach green; see protect the oracle for why that is the most common way agent work fakes success. The second turns “I think it works” into evidence you can read in ten seconds.

The rules and the prompts are identical across tools. What differs is how each tool loads the rules, formats edits, and reviews the result.

Claude Code reads CLAUDE.md first, so create one that imports the shared file. The @path syntax expands the file into context at launch:

@AGENTS.md

Without a CLAUDE.md, Claude Code v2.1.277 and later (both release channels on 2026-10-12) reads AGENTS.md directly (v2.1.281 on Bedrock, Google Cloud, Foundry and LLM gateways; toggle under Project instructions in /config); the import matters once you also keep Claude-specific rules.

Format each TypeScript file the agent edits with a PostToolUse hook in .claude/settings.json (requires Prettier in devDependencies: npm install -D prettier):

{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | grep -E '\\.(ts|js)$' | xargs -r npx --no-install prettier --write"
}
]
}
]
}
}

Give the agent find-references and diagnostics through the typescript-lsp plugin from Anthropic’s official marketplace. The plugin registers the server only, so install the language server first. Pin typescript@6: TypeScript 7.0.2 ships only tsc and no tsserver, which the language server needs (checked 2026-09-26). In a terminal:

Terminal window
npm install -g typescript-language-server typescript@6
claude plugin install typescript-lsp@claude-plugins-official

Before you open a pull request, run /code-review in the session for a second opinion on the diff.

Start each tool on its default model and raise the effort setting before you switch models; the models hub lists the current defaults.

Build a production Express 5 API with an agent, step by step

Section titled “Build a production Express 5 API with an agent, step by step”

Each step is one prompt and the check that proves it. Run them in order: later prompts assume the skeleton and the database module exist.

How do you scaffold an Express 5 service that shuts down cleanly?

Section titled “How do you scaffold an Express 5 service that shuts down cleanly?”

The most common first-pass omission is shutdown. The agent calls app.listen() and nothing else, so every deploy cuts off in-flight requests and leaves database connections open.

A good answer has this core:

const server = app.listen(config.PORT);
function shutdown(signal: NodeJS.Signals) {
logger.info({ signal }, 'shutting down');
setTimeout(() => process.exit(1), 10_000).unref();
server.close(async (err) => {
await prisma.$disconnect();
await redis.quit();
process.exit(err ? 1 : 0);
});
}
process.once('SIGTERM', shutdown);
process.once('SIGINT', shutdown);

You do not need to audit this by eye: the test fails if the request is cut off or the process hangs. Separating app.ts from server.ts also lets supertest drive the app without opening a port.

How do you get validation and error responses clients can rely on?

Section titled “How do you get validation and error responses clients can rely on?”

Agents trained on Express 4 wrap every handler in catchAsync or install express-async-errors. Express 5 already routes rejected promises from handlers to the error middleware, so those wrappers are dead code, and the rule in AGENTS.md keeps them out.

The Prisma mapping is the part agents skip. Without it, a duplicate email becomes a 500 and the client retries a request that can never succeed. The test for a production-mode 500 is what proves that no internal detail reaches the client.

How do you make an agent write refresh-token rotation you can trust?

Section titled “How do you make an agent write refresh-token rotation you can trust?”

Authentication code is where reading the diff feels safest and is least reliable. Specify the attack cases as tests and let them carry the proof.

The reuse test is the one that matters. Without it, an implementation that never revokes the old token passes every other case. For a second, independent check on this code, add a security gate that runs a review agent on every pull request; agent security gates covers the options per tool.

How do you rate-limit an Express API behind a load balancer?

Section titled “How do you rate-limit an Express API behind a load balancer?”

The generated limiter usually works locally and fails in production in one of two ways: counts live in process memory, so each replica allows the full limit, or every request appears to come from the load balancer’s IP address.

The two-instance test proves the shared store; a single-process test passes with the in-memory default and proves nothing about production.

How do you get Prisma transactions that hold under concurrent orders?

Section titled “How do you get Prisma transactions that hold under concurrent orders?”

The agent’s first attempt at “check stock, then decrement” reads the stock, compares it in JavaScript, and writes it back. Two concurrent orders both read stock = 1 and both succeed. A mocked repository cannot show this bug; only a real database under concurrent requests can.

Two Prisma details the agent may not know. Prisma 7 requires a driver adapter: the client is created as new PrismaClient({ adapter: new PrismaPg({ connectionString }) }) with the prisma-client generator and a prisma.config.ts, so pool settings belong to the adapter, not to the connection string. And on 2026-09-26 the npm latest tag of the prisma CLI points at 8.0.0-rc.17, a release candidate of the new Prisma CLI for the Prisma Developer Platform, while @prisma/client latest is 7.10.0. Prisma 8 ships its own agent skills (npx prisma skills sync installs them into .claude/skills/, .cursor/skills/, and .agents/skills/), which makes it attractive for new projects. Choose it deliberately, not because npm install prisma picked it.

To let the agent read your real schema instead of guessing column names, connect a database MCP server to a development database with a read-only role. The database MCP guide compares the servers and their read-only switches. Do not use @modelcontextprotocol/server-postgres: npm marks it deprecated.

How do you move slow work to a BullMQ queue without losing jobs?

Section titled “How do you move slow work to a BullMQ queue without losing jobs?”

The agent’s first version does the work inside the request, or retries a non-idempotent job and sends the email twice.

The idempotency test is what makes retries safe. Without it, the retry that follows a timeout after a successful write produces duplicate files, emails, or charges.

How do you get structured logs that never leak secrets?

Section titled “How do you get structured logs that never leak secrets?”

The agent’s first version logs req.body, so passwords and tokens reach your log search.

A redaction test is the only way to know redaction works; a path that does not match the real object shape fails silently. For dashboards, alerts, and tracing on top of these logs, see monitoring and logging patterns.

How do you verify agent-written Node.js without reading every line?

Section titled “How do you verify agent-written Node.js without reading every line?”

Read the gate output and the test-case lists, not the diff. Each gate below catches a class of bug that looks fine in review.

GateCommandWhat it catches
Typestsc --noEmitWrong shapes between layers, unhandled undefined, misspelled fields
Linteslint . with typescript-eslint type-aware rulesFloating promises (no-floating-promises), misused promises in callbacks (no-misused-promises), unsafe any
Unit and HTTP testsvitest run with supertestStatus codes, error bodies, validation, redaction, shutdown
Integration testsTestcontainers Postgres and RedisWrong SQL, missing unique constraints, races such as overselling, shared rate-limit counts
Dependenciesnpm audit --omit=dev --audit-level=highKnown vulnerabilities in runtime dependencies
Loadnpx autocannon -c 100 -d 30 https://staging.example.com/api/v1/orders against stagingPool exhaustion, event-loop blocking, memory growth
Review agent/code-review (Claude Code), codex review --base main (Codex), Bugbot (Cursor)Logic errors and missing cases the gates cannot express

no-floating-promises deserves special mention: a promise the agent forgot to await becomes an unhandled rejection, and Node.js has terminated the process on unhandled rejections by default since Node.js 15. For stronger evidence that the tests themselves are good, see mutation testing and oracle strength.

Run the first five in CI on every pull request; CI/CD pipeline patterns has the workflow file. The engineer who owns the service signs off on three things: the test-case lists, a green gate, and the load-test result. Human reading goes to the hunks the review agent flags and to code that touches authentication, money, or transactions. Agent PR review covers how to split that work across a team.

When agent-written Node.js breaks in production

Section titled “When agent-written Node.js breaks in production”

Each failure below passes a happy-path test suite. Each has a symptom you can detect and a recovery you can apply.

  • Overselling or double-processing under load. Symptom: negative stock, duplicate orders, or duplicate emails after a traffic spike. Cause: read-then-write logic outside a single atomic statement, or a non-idempotent job that was retried. Recovery: add the concurrent Testcontainers test from the Prisma recipe, then prompt: “Find every place that reads a value, decides in JavaScript, and writes it back. Replace each with a conditional update or a row lock, and add a concurrent test for each.”
  • Connection exhaustion. Symptom: requests hang, then time out, while database CPU is low. Cause: a new PrismaClient() per request or per module, often introduced when the agent adds a new file. Recovery: grep -rn "new PrismaClient" src/ must return one line. A bare grep exits 0 either way, so make the count the test: add "check:prisma": "test $(grep -rn 'new PrismaClient' src/ | wc -l) -eq 1" to scripts and append && npm run check:prisma to check, so the gate fails on a second client.
  • Express 4 idioms in an Express 5 app. Symptom: the server fails to start with a path-to-regexp “Missing parameter name” error, or a catchAsync wrapper hides a bug. Cause: training data dominated by Express 4. Recovery: prompt: “Migrate this codebase to Express 5 idioms: name every wildcard route (/*splat), delete catchAsync and express-async-errors, and change no behaviour. Done means npm run check passes.”
  • Everyone is rate-limited at once. Symptom: a spike of 429s for all users, or express-rate-limit logs a validation error about X-Forwarded-For. Cause: trust proxy is missing or does not match the number of proxies, so every client shares the load balancer’s IP. Recovery: set trust proxy to your real hop count and assert in a test that req.ip is the client address when X-Forwarded-For is set.
  • The install breaks on version tags. Symptom: npm install fails with ERESOLVE because typescript-eslint 8.70.1 declares typescript <6.1.0 while typescript@latest is 7.0.2, or prisma generate output no longer matches the client because the CLI came from the Prisma 8 release candidate. Recovery: pin both with exact versions in package.json, commit the lockfile, and tell the agent in AGENTS.md not to change pinned versions without asking.
  • The agent weakens a test to reach green. Symptom: a loosened assertion, a deleted case, or an added .skip in the same diff as the fix. Recovery: revert the test file, restate the AGENTS.md rule, and make CI fail when a pull request changes an existing *.test.ts without a test-change label.

Did this guide help you?

One tap, no sign-in.

not at all a lot