From nodejs-plugin
Node.js backend conventions: project layout, configuration, logging, routing, and error handling patterns. Apply when implementing or modifying Node.js backend code in projects matching the nodejs stack profile. Use this skill to: - Pick conventional file/folder structure for a new module. - Wire configuration through env vars correctly. - Use the project's existing logger style. - Implement error handling that matches the framework (Express/Fastify/Koa). - Follow async/await patterns consistently. Do NOT use this skill for: - Frontend code (React/Vue/RN have their own conventions). - NestJS-specific patterns (decorators, DI, modules) — nest-plugin owns those. - Database schema design (call out as a sub-task; npm-patterns covers package management, not DB).
How this skill is triggered — by the user, by Claude, or both
Slash command
/nodejs-plugin:node-conventionsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill consolidates idioms that hold across most Node.js backend projects (Express, Fastify, Koa, plain Node). Follow them when adding code; if a project's `CLAUDE.md` contradicts a convention here, the project wins.
This skill consolidates idioms that hold across most Node.js backend projects (Express, Fastify, Koa, plain Node). Follow them when adding code; if a project's CLAUDE.md contradicts a convention here, the project wins.
Typical layout:
project-root/
├── package.json
├── tsconfig.json # if TypeScript
├── src/
│ ├── index.js # entry point — start server, wire app
│ ├── app.js # framework instance (express(), fastify())
│ ├── config.js # env-var reader (single source)
│ ├── routes/ # route definitions, one file per resource
│ ├── controllers/ # request handlers (or merged into routes)
│ ├── services/ # business logic, framework-agnostic
│ ├── middleware/
│ ├── lib/ # shared utilities
│ └── db/ # data access layer
├── test/ OR __tests__/
└── dist/ # build output (TS), gitignored
Mirror what exists. If the project uses a flat layout, do not introduce nested layout for one new file.
Single config module reading from process.env:
// src/config.js
const requiredEnv = ['DATABASE_URL', 'PORT'];
for (const key of requiredEnv) {
if (!process.env[key]) throw new Error(`Missing env var: ${key}`);
}
module.exports = {
port: Number(process.env.PORT),
databaseUrl: process.env.DATABASE_URL,
logLevel: process.env.LOG_LEVEL || 'info',
};
Never read process.env from business logic. Always go through config.
Use the project's existing logger. Detection priority:
pino in deps → const logger = require('pino')()winston in deps → use the configured instancebunyan in deps → use the configured instanceconsole.log/console.error is acceptableNever introduce a new logger as part of feature work without justification in DECISIONS.
Log levels: debug (verbose dev), info (normal flow), warn (recoverable), error (something failed).
// src/routes/health.js
const router = require('express').Router();
const { getHealth } = require('../controllers/health');
router.get('/healthz', getHealth);
module.exports = router;
Mount in src/app.js:
app.use(require('./routes/health'));
// src/routes/health.js
async function healthRoutes(fastify) {
fastify.get('/healthz', async () => ({ status: 'ok' }));
}
module.exports = healthRoutes;
Register in src/app.js:
fastify.register(require('./routes/health'));
Express requires explicit next(err). Two patterns:
Pattern A — wrapper:
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
router.get('/x', asyncHandler(async (req, res) => {
const data = await mayThrow();
res.json(data);
}));
Pattern B — express@5+ native (auto-forwards rejections to next).
Use whatever the project already uses. Don't introduce a wrapper if the project uses express@5.
// src/middleware/error.js
module.exports = (err, req, res, next) => {
logger.error({ err }, 'request failed');
const status = err.status || 500;
res.status(status).json({ error: err.message });
};
Always last in the middleware chain.
setErrorHandlerfastify.setErrorHandler((err, req, reply) => {
logger.error({ err }, 'request failed');
reply.status(err.statusCode || 500).send({ error: err.message });
});
At the request boundary, not in business logic. Use what's installed:
zod → schema.parse(req.body) inside try/catch.joi → schema.validate(req.body, { abortEarly: false }).ajv → compiled validator instance.express-validator middleware on the route.If nothing is installed and BA spec requires validation: add zod (smallest, TS-friendly) and document in DECISIONS.
async/await over .then().catch().require and ESM import in one file. Detect from package.json "type" field.await unless "type": "module" is set.Promise.all([a, b, c]). For settled-on-all, Promise.allSettled.For HTTP servers, handle SIGTERM and SIGINT:
const server = app.listen(port);
const shutdown = async () => {
server.close(() => process.exit(0));
setTimeout(() => process.exit(1), 10_000).unref();
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
Drain in-flight requests, close DB pool, then exit.
app.use((req, res) => res.status(500).send(err.stack)) — never expose stack traces in production responses.process.env.X deep inside business logic.require and import in one file.catch(() => {})).eval, Function() constructor on user input.npx claudepluginhub aratkruglik/claude-sdlc --plugin nodejs-pluginNode.js backend patterns: layered architecture, TypeScript, validation, error handling, security, observability, logging, metrics, deployment. Use when building REST APIs, REST endpoints, middleware, Express/Fastify/Hono/NestJS/Koa servers, tRPC procedures, Bun servers, or server-side TypeScript.
Guides Node.js backend development with Express/Fastify using repository pattern, dependency injection at composition root, and layered core/infra architecture. Includes route handlers, middleware, and error handling patterns.
Guides Node.js architecture decisions: framework selection (Hono, Fastify, Express, NestJS), runtime choice (Node, Bun, Deno), async patterns, security, and layered structure.