Backend Security Basics: 18 Things You Should Not Ignore in Production

Backend security is one of those things developers usually start caring about right after the first weird request hits production at 3AM.

And honestly? Fair enough. Most Express apps start simple:

app.post("/login", loginController);

Then suddenly you realize:

  • Someone is trying 10,000 passwords per minute
  • Your API happily accepts <script> tags
  • Error responses leak internal details
  • Every route is public because you forgot auth middleware
  • A random bot uploaded a 2GB file to memory

Classic.

So here’s a practical backend security checklist for Node.js + Express apps. Not enterprise compliance nonsense. Real stuff that actually matters.


1. Validate ALL Input

Never trust anything coming from:

  • req.body
  • req.query
  • req.params
  • headers
  • uploaded files

EVERYTHING is user-controlled.

Use schema validation libraries like:

  • zod
  • joi
  • yup

Example with zod:

import { z } from "zod";

const registerSchema = z.object({
    email: z.string().email(),
    password: z.string().min(8),
    age: z.number().min(18)
});

app.post("/register", (req, res) => {
    const result = registerSchema.safeParse(req.body);

    if (!result.success) {
        return res.status(400).json({
            message: "Invalid input"
        });
    }

    // safe validated data
    const data = result.data;
});

Without validation, your API becomes a public playground.


2. Prevent XSS (Cross-Site Scripting)

A lot of backend devs think XSS is “frontend stuff”. It isn’t.

If your backend stores dangerous HTML or returns unsafe data, you are part of the problem.

Example of dangerous input:

{
    "bio": "<script>alert('hacked')</script>"
}

If that later gets rendered somewhere without escaping: boom.

What to do

  • Sanitize HTML input
  • Escape output when rendering
  • Never trust rich text editors blindly

Useful package:

npm install sanitize-html
import sanitizeHtml from "sanitize-html";

const cleanBio = sanitizeHtml(req.body.bio);

If you don’t need HTML: remove it completely.


3. Enable Rate Limiting

Without rate limiting, your login endpoint becomes a free brute-force machine.

Use:

npm install express-rate-limit
import rateLimit from "express-rate-limit";

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 100
});

app.use(limiter);

For auth routes, be much stricter:

const loginLimiter = rateLimit({
    windowMs: 10 * 60 * 1000,
    max: 5,
    message: "Too many login attempts"
});

app.use("/login", loginLimiter);

Otherwise someone will absolutely try:

password123
password1234
password12345

...for several million requests.


4. Use Helmet

Helmet adds important security headers automatically.

npm install helmet
import helmet from "helmet";

app.use(helmet());

This helps against:

  • XSS
  • clickjacking
  • MIME sniffing
  • some CSP-related attacks

One middleware. Big value.


5. Never Store Plain Passwords

Not even “temporarily”. Not even “for testing”. Not even “just locally”.

Hash passwords with bcrypt.

npm install bcrypt
import bcrypt from "bcrypt";

const hashedPassword = await bcrypt.hash(password, 12);

And compare safely:

const isValid = await bcrypt.compare(
    enteredPassword,
    user.password
);

If your database leaks and passwords are plain text, your users are cooked.


6. Use HTTP-Only Secure Cookies

If using cookies for auth:

  • set httpOnly
  • set secure
  • set sameSite
res.cookie("token", token, {
    httpOnly: true,
    secure: true,
    sameSite: "strict"
});

This reduces token theft risks significantly.


7. Protect Against SQL Injection

Never concatenate raw user input into queries.

Bad:

const query = `
SELECT * FROM users
WHERE email = '${email}'
`;

Very bad.

Use ORM/query builders:

  • Prisma
  • Drizzle
  • TypeORM
  • Knex

Or parameterized queries:

const user = await pool.query(
    "SELECT * FROM users WHERE email = $1",
    [email]
);

8. Hide Stack Traces in Production

This:

{
    "error": "Cannot read properties of undefined",
    "stack": "... giant stack trace ..."
}

...is free reconnaissance for attackers.

Instead:

app.use((err, req, res, next) => {
    console.error(err);

    res.status(500).json({
        message: "Internal server error"
    });
});

Log internally. Expose minimally.


9. Use Environment Variables Properly

Never hardcode:

  • JWT secrets
  • database credentials
  • API keys

Use:

.env
JWT_SECRET=super_secret_value

And load them:

import dotenv from "dotenv";

dotenv.config();

Also:

  • never commit .env
  • rotate secrets occasionally
  • use different secrets per environment

10. Enable CORS Correctly

This is dangerous:

app.use(cors({
    origin: "*"
}));

Especially with credentials.

Prefer:

app.use(cors({
    origin: [
        "https://yourfrontend.com"
    ],
    credentials: true
}));

Be explicit.


11. Limit Request Body Size

Otherwise attackers can send absurd payloads.

app.use(express.json({
    limit: "1mb"
}));

Without limits, memory usage can spike fast.


12. Secure File Uploads

File uploads are attack magnets.

Check:

  • file type
  • file extension
  • size
  • virus scanning (important)

Never trust:

evil.exe.png

Also avoid storing uploads directly inside public folders.


13. Use HTTPS Everywhere

No HTTPS = traffic readable by others.

In production:

  • always use TLS
  • redirect HTTP → HTTPS
  • use secure cookies only over HTTPS

Modern apps without HTTPS are basically driving without brakes.


14. Add Authentication & Authorization Properly

Authentication answers: “Who are you?”

Authorization answers: “What are you allowed to do?”

A very common bug:

// user is authenticated
// but can delete ANY account

DELETE /users/123

Always check ownership/permissions.

if (req.user.id !== targetUser.id) {
    return res.status(403).json({
        message: "Forbidden"
    });
}

15. Log Suspicious Activity

You should log:

  • failed logins
  • permission failures
  • rate limit violations
  • token failures
  • admin actions

Not just for debugging. For incident investigation later.


16. Keep Dependencies Updated

Some npm packages are abandoned security disasters.

Run:

npm audit

And:

npm audit fix

Also:

  • remove unused packages
  • avoid random tiny libraries
  • check maintenance activity before installing

Your app security is partially outsourced to strangers on npm. Choose wisely.


17. Add CSRF Protection (If Using Cookies)

If auth is cookie-based, CSRF matters.

Use:

  • CSRF tokens
  • sameSite cookies
  • origin validation

JWT in Authorization headers avoids many CSRF problems, but cookie auth absolutely needs protection.


18. Don’t Trust Client-Side Validation

Frontend validation is UX. Backend validation is security.

Never assume “The frontend would never send that.”

Attackers are not using your frontend.


Quick Minimal Express Security Stack

import express from "express";
import helmet from "helmet";
import cors from "cors";
import rateLimit from "express-rate-limit";

const app = express();

app.use(helmet());

app.use(cors({
    origin: ["https://yourfrontend.com"],
    credentials: true
}));

app.use(express.json({
    limit: "1mb"
}));

app.use(rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 100
}));

This alone already puts you ahead of a shocking number of production APIs.


Final Thought

Security is not one giant feature you “finish”. It’s a collection of small defensive decisions.

Most real-world backend breaches happen because of:

  • missing validation
  • bad auth checks
  • misconfigured infrastructure
  • exposed secrets
  • outdated dependencies

Not because hackers used movie-style keyboard magic.

Good backend security is mostly disciplined engineering. Boring engineering. Paranoid engineering.

And honestly, that’s exactly what makes systems survive production.

1 Comments

  • Amir Zare
    23 Jun, 2026

    Good checklist. One thing I would add is: use the correct HTTP methods. Don't use GET for actions that change state. Learning what each method is supposed to do is definitely worth it

    • Good addition. HTTP semantics get ignored way too often in real APIs. I’ve been a first-hand witness to that kind of monstrosity 😄

Leave a Comment