Security Best Practices for Full-Stack Applications
Security isn't optional—it's fundamental. A single vulnerability can compromise your entire application and user data. After dealing with security incidents and implementing security measures in production systems, I've learned what actually matters.
The Security Mindset
Security is not a feature you add—it's a way of thinking. Every decision should consider security implications.
Principles
- Defense in depth - Multiple layers of security
- Least privilege - Minimum access necessary
- Fail securely - Default to secure state
- Never trust input - Validate everything
- Keep it simple - Complexity breeds vulnerabilities
Authentication and Authorization
Password Security
// Bad: Plain text passwords
const user = {
email: 'user@example.com',
password: 'password123' // Never!
};
// Good: Hashed passwords
const bcrypt = require('bcrypt');
async function createUser(email, password) {
const hashedPassword = await bcrypt.hash(password, 10);
// Store hashedPassword, never plain text
}
async function verifyPassword(plainPassword, hashedPassword) {
return await bcrypt.compare(plainPassword, hashedPassword);
}
JWT Best Practices
// Good: Secure JWT implementation
const jwt = require('jsonwebtoken');
// Generate token
function generateToken(user) {
return jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{
expiresIn: '1h',
issuer: 'your-app',
audience: 'your-app-users'
}
);
}
// Verify token
function verifyToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'your-app',
audience: 'your-app-users'
});
} catch (error) {
throw new Error('Invalid token');
}
}
Session Management
// Good: Secure session configuration
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // Not accessible via JavaScript
maxAge: 3600000, // 1 hour
sameSite: 'strict' // CSRF protection
}
}));
Input Validation
Never Trust User Input
// Bad: No validation
app.post('/api/users', (req, res) => {
const user = req.body;
db.query('INSERT INTO users VALUES (?, ?)', [user.email, user.name]);
});
// Good: Validate everything
const { body, validationResult } = require('express-validator');
app.post('/api/users',
[
body('email').isEmail().normalizeEmail(),
body('name').trim().isLength({ min: 1, max: 100 }).escape(),
body('age').isInt({ min: 0, max: 120 })
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process validated input
}
);
SQL Injection Prevention
// Bad: SQL injection vulnerability
app.get('/api/users', (req, res) => {
const query = `SELECT * FROM users WHERE id = ${req.query.id}`;
db.query(query); // Dangerous!
});
// Good: Parameterized queries
app.get('/api/users', (req, res) => {
db.query('SELECT * FROM users WHERE id = $1', [req.query.id]);
});
XSS Prevention
// Bad: XSS vulnerability
app.get('/search', (req, res) => {
const query = req.query.q;
res.send(`<h1>Results for: ${query}</h1>`); // Dangerous!
});
// Good: Escape output
const escapeHtml = require('escape-html');
app.get('/search', (req, res) => {
const query = escapeHtml(req.query.q);
res.send(`<h1>Results for: ${query}</h1>`);
});
// Or use a template engine that auto-escapes
res.render('search', { query: req.query.q }); // Auto-escaped
Data Protection
Encryption at Rest
// Encrypt sensitive data
const crypto = require('crypto');
function encrypt(text) {
const algorithm = 'aes-256-gcm';
const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}
Encryption in Transit
Always use HTTPS:
// Force HTTPS
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
Sensitive Data Handling
// Never log sensitive data
function logUserAction(userId, action) {
logger.info('User action', {
userId, // OK
action, // OK
// password: user.password, // Never!
// creditCard: order.cardNumber, // Never!
// ssn: user.ssn // Never!
});
}
API Security
Rate Limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
app.use('/api/', limiter);
CORS Configuration
// Good: Restrictive CORS
const cors = require('cors');
app.use(cors({
origin: process.env.ALLOWED_ORIGINS.split(','),
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
API Key Security
// Store API keys securely
const apiKeys = new Map();
function validateApiKey(req, res, next) {
const apiKey = req.header('X-API-Key');
if (!apiKey || !apiKeys.has(apiKey)) {
return res.status(401).json({ error: 'Invalid API key' });
}
// Rate limit per API key
const keyData = apiKeys.get(apiKey);
if (keyData.requests > keyData.limit) {
return res.status(429).json({ error: 'Rate limit exceeded' });
}
keyData.requests++;
next();
}
Dependency Security
Keep Dependencies Updated
# Check for vulnerabilities
npm audit
# Fix automatically
npm audit fix
# Update dependencies
npm update
Use Lock Files
// package-lock.json should be committed
// Ensures consistent, secure versions
Review Dependencies
- Check package popularity
- Review maintenance status
- Check for known vulnerabilities
- Prefer well-maintained packages
Error Handling
Don't Leak Information
// Bad: Leaks stack trace
app.use((err, req, res, next) => {
res.status(500).json({
error: err.message,
stack: err.stack // Never in production!
});
});
// Good: Generic error messages
app.use((err, req, res, next) => {
logger.error('Error', { error: err, requestId: req.id });
res.status(500).json({
error: 'An error occurred',
requestId: req.id // For tracking
});
});
Security Headers
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
Real-World Example
Challenge: E-commerce platform handling payment data, frequent security audits.
Security Measures Implemented:
- HTTPS everywhere - All traffic encrypted
- Input validation - All user input validated
- Parameterized queries - No SQL injection
- Rate limiting - Prevent abuse
- Secure sessions - HttpOnly, Secure cookies
- Dependency updates - Regular security patches
- Security headers - Helmet.js configuration
- Error handling - No information leakage
- Logging - Security events logged
- Regular audits - Security testing
Result: Passed security audits, no security incidents.
Security Checklist
- Passwords hashed (bcrypt, argon2)
- HTTPS enforced
- Input validated
- SQL injection prevented
- XSS prevented
- CSRF protection
- Rate limiting
- Secure headers
- Dependencies updated
- Secrets in environment variables
- Error messages don't leak info
- Authentication implemented correctly
- Authorization checks in place
- Sensitive data encrypted
- Security logging enabled
Best Practices Summary
- Validate input - Never trust user data
- Use HTTPS - Encrypt in transit
- Hash passwords - Never store plain text
- Keep dependencies updated - Security patches
- Limit access - Least privilege principle
- Log security events - Monitor for issues
- Regular audits - Test your security
- Stay informed - Security is evolving
Conclusion
Security is not a one-time task—it's an ongoing process. The threats evolve, and so must your defenses. The key is to:
- Think security first - Consider security in every decision
- Follow best practices - Don't reinvent the wheel
- Stay updated - Security landscape changes
- Test regularly - Find vulnerabilities before attackers
Remember: Security is everyone's responsibility. Every developer should understand security basics and apply them consistently.
What security challenges have you faced? What practices have been most effective for your applications?
Related Posts
Modern Authentication: OAuth 2.0, JWT, and Session Management
Master modern authentication patterns including OAuth 2.0, JWT tokens, and session management. Learn when to use each approach and how to implement them securely.
AI Security and Privacy: Building Trustworthy AI Applications
Understand critical security and privacy considerations when building AI applications. Learn about prompt injection attacks, data privacy regulations, model safety, and how to build AI systems users can trust.
API Design Principles: Creating Developer-Friendly REST APIs
Learn the principles and patterns for designing REST APIs that developers love to use. From URL structure to error handling, this guide covers it all.
REST API Design Best Practices: Building Developer-Friendly APIs
Learn how to design REST APIs that are intuitive, maintainable, and developer-friendly. From URL structure to error handling, master the principles that make APIs great.
Serverless Architecture: When to Use and When to Avoid
A practical guide to serverless architecture. Learn when serverless makes sense, its trade-offs, and how to build effective serverless applications.
GraphQL vs REST: Making the Right API Choice in 2025
A comprehensive comparison of GraphQL and REST APIs in 2025. Learn when to use each approach, their trade-offs, and how to make the right decision for your project.