Node.js Best Practices
You know the pieces. This lesson ties them together into habits that keep Node apps fast, safe, and maintainable.
Structure by Feature
Group files by what they do, not what they are:
src/
users/
users.routes.js
users.service.js
users.repo.js
orders/
orders.routes.js
orders.service.js
app.js
server.js
Each feature owns its routes, logic, and data access. Easy to find, easy to delete.
Separate Server from App
Keep the Express app exportable and the listening call separate — this makes testing possible:
// app.js
export const app = express();
app.get("/health", (req, res) => res.json({ ok: true }));
// server.js
import { app } from "./app.js";
app.listen(process.env.PORT ?? 3000);
Tests can import app without opening a port.
Handle Errors Centrally
Log once, respond once:
process.on("unhandledRejection", (err) => {
console.error("Unhandled rejection:", err);
process.exit(1);
});
In Express, route all errors through one handler and give each error a status code.
Security Essentials
- Validate and sanitize input — Never trust the client.
- Use
helmet— Sets safe HTTP headers. - Enable CORS deliberately — Allow only known origins.
- Rate-limit — Protect auth and write endpoints.
- Hash passwords — Use
bcryptorargon2, never plain text. - Keep dependencies updated — Run
npm auditregularly.
Logging
Structured logs beat console.log in production:
console.log(JSON.stringify({ level: "info", msg: "user created", id: user.id }));
Tools like pino add levels, timestamps, and performance.
Graceful Shutdown
Close connections before exiting so in-flight requests finish:
process.on("SIGTERM", () => {
server.close(() => {
console.log("Shutting down");
process.exit(0);
});
});
Performance
- Do not block the event loop — Offload CPU work to workers.
- Cache expensive results — In memory or Redis.
- Use streams — For large files and responses.
- Connection pooling — Reuse database connections.
Best Practices
- One responsibility per module.
- Validate all input.
- Log with structure and levels.
- Shut down gracefully.
- Automate tests and security checks in CI.
Common Mistakes
- Doing everything in
server.js. - Trusting
req.bodyorreq.query. - Blocking the event loop with sync code.
- Leaving secrets in the repo.
- Skipping error handling until production breaks.