Security Is Not Optional
Every Node.js app that touches the internet is a target. Security isn't an afterthought — it's a first-class concern from day one. This guide covers the most important security practices for Node.js/Express applications.
1. Validate and Sanitize All Input
Never trust input from users, APIs, or databases. Use a validation library like Zod:
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(1).max(100).trim(),
email: z.string().email().toLowerCase(),
age: z.number().int().min(0).max(150),
});
app.post('/users', (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
// result.data is now safe to use
});
2. Use Parameterized Queries (Never Template SQL)
// NEVER do this — SQL injection vulnerability
const user = await db.query(
`SELECT * FROM users WHERE email = '${email}'`
);
// Always use parameterized queries
const user = await db.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
// Or use an ORM (Prisma, Drizzle, TypeORM)
const user = await prisma.user.findUnique({ where: { email } });
3. Set Security Headers with Helmet
npm install helmet
import helmet from 'helmet';
app.use(helmet());
Helmet sets a dozen security headers automatically: Content-Security-Policy, X-Frame-Options, X-XSS-Protection, and more.
4. Rate Limiting
Prevent brute force and DoS attacks:
import rateLimit from 'express-rate-limit';
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // max 10 attempts per window
message: 'Too many login attempts. Please try again later.',
});
app.post('/auth/login', authLimiter, loginHandler);
5. Secure JWT Handling
import jwt from 'jsonwebtoken';
// Sign with RS256 (asymmetric) for production
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_PRIVATE_KEY,
{
algorithm: 'RS256',
expiresIn: '15m', // Short-lived access tokens
issuer: 'your-app',
}
);
// Store refresh tokens in httpOnly cookies — not localStorage
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});
6. Hash Passwords with bcrypt
import bcrypt from 'bcryptjs';
// Hash on registration
const hash = await bcrypt.hash(password, 12); // cost factor 12
// Verify on login
const isValid = await bcrypt.compare(inputPassword, storedHash);
Never store plain text passwords or use MD5/SHA1 for passwords.
7. Environment Variables — Never Hardcode Secrets
// Bad — hardcoded secrets
const apiKey = 'sk-abc123def456';
// Good
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) throw new Error('OPENAI_API_KEY not set');
Use a .env file locally and a secrets manager (AWS Secrets Manager, Doppler, Vercel env vars) in production. Add .env to .gitignore.
8. Audit Dependencies Regularly
npm audit # check for known vulnerabilities
npm audit fix # auto-fix where possible
npx snyk test # more comprehensive scanning
Run npm audit in CI so you catch new vulnerabilities before they ship.
9. Avoid eval() and Function()
// Never do this — remote code execution risk
eval(userInput);
new Function(userInput)();
10. Use HTTPS Everywhere
In production, always terminate TLS. Use Nginx, a load balancer, or Cloudflare in front of your Node.js server. Never serve credentials over HTTP.
Conclusion
Security is a checklist you work through systematically: validate input, parameterize queries, set security headers, rate limit, handle JWTs carefully, hash passwords, keep secrets out of code, audit deps. Do these 10 things and you'll have eliminated the vast majority of common vulnerabilities.