7 Best Practices for Writing Secure Backend Code in Node.js

Published on 17th March, 2025

Security is one of the most important things in backend development. A small mistake can expose your entire system to hackers. If you're a beginner, don’t worry! In this post, I'll explain 7 simple best practices to keep your Node.js backend secure with easy-to-understand code examples.

1. Always Hash Passwords

Storing plain-text passwords is a huge security risk. If your database gets leaked, all user accounts will be compromised. Instead, always hash passwords before saving them.

Example:

const bcrypt = require('bcrypt');

const hashPassword = async (password) => {
  const saltRounds = 10;
  return await bcrypt.hash(password, saltRounds);
};

// Example usage
hashPassword("mySecurePassword").then(console.log);

This ensures that even if your database is leaked, the passwords will remain safe.

2. Use Environment Variables for Sensitive Data

Hardcoding API keys, database credentials, or secrets directly in your code is a bad practice. If someone gets access to your code, they can see all your secrets. Instead, store them in a .env file.

Example:

Create a .env file:

DB_PASSWORD=supersecretpassword
JWT_SECRET=myjwtsecretkey

Load it in your Node.js app:

require('dotenv').config();

console.log("Database Password:", process.env.DB_PASSWORD);

📌 Never commit your .env file to Git! Add it to .gitignore.

3. Validate and Sanitize User Inputs

User input is one of the main ways hackers can attack your application. If you don’t validate input, they can inject malicious code (SQL injection, XSS attacks, etc.).

Example:

Using Joi to validate user data:

const Joi = require('joi');

const schema = Joi.object({
  username: Joi.string().alphanum().min(3).max(30).required(),
  email: Joi.string().email().required(),
});

const userInput = { username: "admin123", email: "admin@example.com" };
const { error } = schema.validate(userInput);

if (error) {
  console.error("Invalid input:", error.details[0].message);
} else {
  console.log("Input is valid!");
}

This makes sure users can’t send unexpected or harmful data to your backend.

4. Implement Rate Limiting to Prevent Abuse

Without rate limiting, attackers can send thousands of requests per second, causing brute-force attacks and DDoS attacks.

Example:

Using express-rate-limit to limit API requests:

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

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per window
  message: "Too many requests, please try again later.",
});

app.use(limiter);

Now, users can't spam your API with unlimited requests.

5. Use Proper Authentication & Authorization

If your authentication system is weak, users can impersonate others or access data they shouldn't see. Use JWT (JSON Web Token) to securely manage authentication.

Example:

Generating and verifying a JWT:

const jwt = require('jsonwebtoken');

const generateToken = (user) => {
  return jwt.sign({ id: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '1h' });
};

// Middleware to protect routes
const authenticate = (req, res, next) => {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) return res.status(401).json({ message: "Unauthorized" });

  jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
    if (err) return res.status(403).json({ message: "Invalid token" });
    req.user = decoded;
    next();
  });
};

This ensures that only authenticated users can access protected routes.

6. Regularly Update Dependencies

Outdated dependencies can have security vulnerabilities that hackers exploit. Always keep your packages up to date.

Example:

Check for outdated packages:

npm outdated

Update all packages:

npm update

Check for security issues:

npm audit

If vulnerabilities are found, fix them with:

npm audit fix

📌 Always update your dependencies, but test your app after updates to avoid breaking changes.

7. Avoid Exposing Internal Errors in API Responses

If you return raw error messages, attackers can learn about your system’s structure and find weak points. Instead, send a generic error message and log the real error internally.

Example:

app.use((err, req, res, next) => {
  console.error(err.stack); // Log it for debugging (but not in API response)
  res.status(500).json({ message: "Something went wrong. Please try again later." });
});

This way, users won’t see sensitive error details, but developers can still debug issues from the logs.

Final Thoughts

Security is not a one-time thing, it’s a habit. By following these 7 best practices, you can protect your backend from common security threats and keep user data safe.

Which security practice do you think is the most important? Or do you have any other security tips? Let’s discuss in the comments! 🚀

Comments

Please login to publish your comment!

By logging in, you agree to our Terms of Service and Privacy Policy.


No comments here!