Security should be built into your API from day one, not added as an afterthought. This guide covers essential security practices that every production API should implement.
import jwt from 'jsonwebtoken';
import { rateLimit } from 'express-rate-limit';
// Secure JWT configuration
const jwtOptions = {
expiresIn: '15m',
issuer: 'your-app.com',
audience: 'api.your-app.com',
algorithm: 'RS256' // Use asymmetric signing
};
function generateToken(payload: any): string {
return jwt.sign(payload, privateKey, jwtOptions);
}
Never expose API keys in client-side code or version control. Use environment variables and secure key rotation practices.
// Hash API keys before storage
import crypto from 'crypto';
function hashApiKey(key: string): string {
return crypto
.createHash('sha256')
.update(key)
.digest('hex');
}
function validateApiKey(providedKey: string, storedHash: string): boolean {
const providedHash = hashApiKey(providedKey);
return crypto.timingSafeEqual(
Buffer.from(providedHash),
Buffer.from(storedHash)
);
}
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email().max(255),
password: z.string().min(8).max(128),
name: z.string().min(1).max(100)
});
app.post('/users', async (req, res) => {
try {
const validatedData = CreateUserSchema.parse(req.body);
// Process validated data
} catch (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.issues
});
}
});
const createRateLimit = (windowMs: number, max: number) =>
rateLimit({
windowMs,
max,
message: 'Too many requests from this IP',
standardHeaders: true,
legacyHeaders: false,
// Custom key generator for authenticated users
keyGenerator: (req) => {
return req.user?.id || req.ip;
}
});
// Different limits for different endpoints
app.use('/api/auth', createRateLimit(15 * 60 * 1000, 5)); // 5 per 15min
app.use('/api/search', createRateLimit(60 * 1000, 100)); // 100 per minute
// Always use HTTPS in production
app.use((req, res, next) => {
if (process.env.NODE_ENV === 'production' && !req.secure) {
return res.redirect(301, `https://${req.headers.host}${req.url}`);
}
next();
});
// Encrypt sensitive data before storage
import { encrypt, decrypt } from './crypto-utils';
async function storeUserData(userData: UserData) {
const encryptedData = {
...userData,
email: encrypt(userData.email),
phone: encrypt(userData.phone)
};
return await db.user.create({ data: encryptedData });
}
// Use parameterized queries (with Prisma, this is automatic)
const user = await prisma.user.findUnique({
where: { email: userEmail } // Safe from SQL injection
});
// If using raw SQL, always use parameters
const result = await prisma.$queryRaw`
SELECT * FROM users
WHERE email = ${userEmail}
AND status = ${status}
`;
import winston from 'winston';
const securityLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'security.log' })
]
});
// Log security events
function logSecurityEvent(event: string, details: any, req: Request) {
securityLogger.warn('Security Event', {
event,
ip: req.ip,
userAgent: req.get('User-Agent'),
userId: req.user?.id,
timestamp: new Date().toISOString(),
details
});
}
Implement enterprise-grade security without the complexity. Conduit.im provides built-in security features including rate limiting, API key management, and audit logging.
import helmet from '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
}
}));
API security is an ongoing process, not a one-time implementation. Regular security audits, dependency updates, and monitoring are essential for maintaining a secure production environment.
Remember: security is only as strong as its weakest link. Implement defense in depth and always follow the principle of least privilege.