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.bodyreq.queryreq.params- headers
- uploaded files
EVERYTHING is user-controlled.
Use schema validation libraries like:
zodjoiyup
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
sameSitecookies- 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.
Leave a Comment