Node.js backend rules for AI coding agents

Most Node.js bugs an agent introduces are quiet ones: a promise nobody awaits, an async callback passed to forEach, a JSON.parse on user input that throws and takes the request with it. The Node.js backend rules target HTTP services and workers in TypeScript or JavaScript and are mostly about keeping async code honest and the event loop free.

They also cover the setup work agents skip: checking engines and .nvmrc before using a newer API, reading configuration from the environment once and validating it at startup, and keeping routes, services and data access in separate layers so services never touch req or res. Testing guidance is simple: use the runner the repo already has rather than adding a second one.

Folder: rules/node-backend/ · node-backend.mdc, CLAUDE.md, AGENTS.md

What the Node.js rules tell your agent

First rule of each section in node-backend.mdc, quoted as shipped.

Mistakes these rules are written to prevent

Source: “Pitfalls to avoid”.

Checks the agent runs before it says done

Example commands from the Node.js file (swap in your repo's own):

Run lint, the type check if TypeScript, and the test suite. Start the server once and hit the changed route if you touched routing or middleware. Say which commands you ran and paste failures instead of summarizing them away.

Before and after: Node.js: async error handling in Express routes

Prompt card 05 · Stack: Node.js, Express 4 or 5 · Type: Review and fix · Best model: a frontier reasoning model

What the card is for

Find rejected promises that never reach the Express error handler, handlers that can send two responses, and error responses that leak stack traces. On Express 4 an unhandled rejection in an async handler leaves the request hanging until the client times out.

Worked example from the card

Before

router.post("/invites", async (req, res) => {
  const body = JSON.parse(req.body.payload);
  const team = await teams.get(body.teamId);
  if (!team) res.status(404).json({ error: "team not found" });
  mailer.send(body.email, inviteTemplate(team));
  res.status(201).json({ ok: true });
});

app.use((err, req, res) => {
  res.status(500).json({ error: err.message, stack: err.stack });
});

Expected AI output (abridged)

| POST /invites | 2 | JSON.parse throws on bad input -> unhandled rejection on Express 4,
|               |   | request hangs. Fix: validate and return 400.
| POST /invites | 4 | Missing return after 404 -> second response, ERR_HTTP_HEADERS_SENT.
| POST /invites | 5 | mailer.send not awaited -> failures are lost and the client gets 201
|               |   | even when no email was sent. Await it or enqueue a job.
| error handler | - | Three parameters, so Express treats it as normal middleware and never
|               |   | calls it for errors. Also leaks message and stack.
Recommendation: on Express 4, add try/catch with next(err) per handler now; plan the
Express 5 upgrade to remove the boilerplate.

After

const InviteBody = z.object({ teamId: z.string().uuid(), email: z.string().email() });

router.post("/invites", async (req, res, next) => {
  try {
    const parsed = InviteBody.safeParse(req.body);
    if (!parsed.success) return res.status(400).json({ error: "invalid_body" });
    const team = await teams.get(parsed.data.teamId);
    if (!team) return res.status(404).json({ error: "team_not_found" });
    await jobs.enqueue("send-invite", { teamId: team.id, email: parsed.data.email });
    return res.status(201).json({ ok: true });
  } catch (err) {
    return next(err);
  }
});

app.use((err, req, res, next) => {
  if (err instanceof NotFoundError) return res.status(404).json({ error: err.code });
  req.log.error({ err }, "unhandled error");
  return res.status(500).json({ error: "internal_error" });
});

Excerpt from card 05; the prompt itself is in the pack.

More Node.js prompt cards in the pack

Using these rules in Cursor, Claude Code and AGENTS.md tools

Cursor

Put _base.mdc and node-backend.mdc in .cursor/rules/. Globs: src/**/*.ts, src/**/*.js, src/**/*.mjs, server/**/*.ts, package.json.

Claude Code

Root CLAUDE.md, base first. Path-scoped variant .claude/rules/node-backend.md for src/**/*.{ts,js,mjs}, server/**/*.ts; PostToolUse hook: npx prettier --write <file>.

AGENTS.md tools (Codex, Gemini CLI, Aider)

Base then stack in one AGENTS.md. Gemini CLI needs context.fileName; Aider needs --read AGENTS.md. Put this file in services/api/ in a monorepo, or the repo root. Agents use the AGENTS.md nearest to the file being edited, so a nested copy overrides a root one for that subtree.

Related stacks

← All stacks