10 Node.js Best Practices I Wish I Knew Sooner
Sachin Kasana

Sachin Kasana @sachinkasana

About: Principal Engineer by day, DevTools builder by night 🌙 | Love working with Node.js, React & AI | Sharing insights on clean code, performance, and web development. https://json-formatter-dev.vercel.ap

Location:
india
Joined:
Jun 30, 2022

10 Node.js Best Practices I Wish I Knew Sooner

Publish Date: Apr 28
1 0

Build scalable, secure, and production-ready Node.js apps like a pro.

✨ Introduction

When I wrote my first Node.js app, I thought if it worked, it was done. Fast forward to handling production workloads, scaling APIs, and debugging midnight crashes — I realized there’s a big difference between code that runs and code that’s built right.

These aren’t just beginner tips — they’re hard-earned lessons from real-world projects. If you’re serious about writing clean, scalable, and maintainable Node.js applications, these 10 best practices will save you countless hours (and headaches).

1️⃣ Use AsyncLocalStorage for Better Context Handling

Tired of passing req.userId or traceId through every function?

Meet AsyncLocalStorage — Node.js’s built-in way to handle request-scoped data without messy parameter chains.

const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();

app.use((req, res, next) => {
  asyncLocalStorage.run(new Map(), () => {
    asyncLocalStorage.getStore().set('requestId', req.headers['x-request-id']);
    next();
  });
});

// Anywhere in your code
const requestId = asyncLocalStorage.getStore().get('requestId');
Enter fullscreen mode Exit fullscreen mode

Pro Tip: Perfect for logging, tracing, and multi-tenant apps!

2️⃣ Implement Graceful Shutdowns — Stop Killing Your App Brutally

Hitting Ctrl+C or a container stop shouldn't corrupt your data or leave sockets hanging.

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

async function shutdown() {
  console.log('Gracefully shutting down...');
  await db.close();
  server.close(() => process.exit(0));
}
Enter fullscreen mode Exit fullscreen mode

🚨 Common Mistake: Forgetting to handle SIGINT leads to dangling DB connections.

3️⃣ Use Dependency Injection for Cleaner, Testable Code

Hard-coded dependencies make testing and scaling painful.

Embrace lightweight Dependency Injection (DI).

// Instead of this
const userService = new UserService(new UserRepo());

// Do this
function createUserController({ userService }) {
   return (req, res) => userService.create(req.body);
}
Enter fullscreen mode Exit fullscreen mode

Pro Tip: Use libraries like awilix for structured DI.

4️⃣ Batch & Debounce Expensive Operations

Why hit your DB/API 100 times when you can batch them?

const queue = [];
function batchInsert(data) {
  queue.push(data);
  if (queue.length >= 10) {
    db.insertMany(queue);
    queue.length = 0;
  }
}
Enter fullscreen mode Exit fullscreen mode

Use Case: Logging, notifications, bulk writes.

5️⃣ Structure Your App for Scale — Even If It’s Small Now

Forget controllers, models, and utils dumped in one folder.

Adopt feature-based or domain-driven structures:

/users
   controller.js
   service.js
   repo.js
/orders
   controller.js
   ...
Enter fullscreen mode Exit fullscreen mode

🏗️ Pro Tip: This keeps things modular, especially when your app grows.

6️⃣ Master Error Handling with Custom Error Classes

Throwing plain Error objects everywhere? That’s messy.

Create custom error classes for clarity and centralized handling:

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
  }
}

// Usage
throw new AppError('User not found', 404);
Enter fullscreen mode Exit fullscreen mode

In your Express error middleware:

app.use((err, req, res, next) => {
  res.status(err.statusCode || 500).json({ error: err.message });
});
Enter fullscreen mode Exit fullscreen mode

Pro Tip: Standardize errors for APIs — your frontend team will love you!

7️⃣ Leverage Node.js Streams for Large Data Processing

Reading big files with fs.readFile? Say hello to memory leaks.

Use Streams to process data efficiently:

const fs = require('fs');
const readStream = fs.createReadStream('large-file.csv');

readStream.on('data', chunk => {
  // Process chunk
});
Enter fullscreen mode Exit fullscreen mode

🚀 Use Case: File uploads, CSV parsing, log processing.

8️⃣ Secure Your App by Default (Helmet, Rate Limits, Sanitization)

Don’t wait to get burned by an attack. Apply basic security middlewares:

const helmet = require('helmet');
const rateLimit = require('express-rate-limit');

app.use(helmet());
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
Enter fullscreen mode Exit fullscreen mode

🔒 Pro Tip: Always sanitize inputs to avoid NoSQL/SQL injection.

9️⃣ Use PM2 for Production Process Management

Running node app.js in production? Please don’t.

Use PM2 to manage, monitor, and auto-restart your app:

pm2 start app.js --name my-app
pm2 save
pm2 startup
Enter fullscreen mode Exit fullscreen mode
  • Handles crashes gracefully.
  • Supports clustering for multi-core CPUs.
  • Built-in monitoring dashboard.

Bonus: Integrate PM2 logs with centralized logging tools.

🔟 Logging Without Correlation IDs = Debugging Nightmare

Logs are useless if you can’t trace a request across services.

Generate a unique request ID for every incoming request:

app.use((req, res, next) => {
  req.id = uuid.v4();
  next();
});

// In logger
logger.info(`Request ID: ${req.id} - User fetched`);
Enter fullscreen mode Exit fullscreen mode

🛠️ Pro Tip: Use AsyncLocalStorage + winston or pino for elegant tracing.

🎁 Bonus Tip: Use node --inspect Like a Debugging Ninja 🐞

Stop flooding your console with console.log.

Use Node.js’s built-in debugger:

node --inspect app.js
Enter fullscreen mode Exit fullscreen mode
  • Open chrome://inspect in Chrome.
  • Set breakpoints, step through code like a pro.

Pro Tip: Combine with VSCode’s debugging for a seamless experience.

🎯 Conclusion

These aren’t just “nice-to-have” tips — they’re survival tactics for any serious Node.js developer.

Whether you’re building side projects or managing production systems, following these best practices will make your code cleaner , more scalable , and save you from those “why is this breaking at 2 AM?” moments.

💬 Let’s Chat & Boost Productivity!

I love sharing real-world tips on Node.js , clean code, performance optimization, and crafting handy developer tools.

If you found these best practices helpful:

✅ Follow me on Medium for weekly dev insights

✅ Explore my portfolio & free tools like JSON Formatter 🚀 — because every dev deserves clean, readable JSON!

✅ Visit: sachinkasana-dev.vercel.app

✅ Connect on LinkedIn — always open to tech chats, collaborations, or geeking out over JavaScript! 😄

🚀 Thanks for Reading!

Happy Coding! 😄


Comments 0 total

    Add comment