Back to Blog
API SecuritySecurityAuthenticationBest Practices

API Security Checklist for Production Applications

Secure your APIs against common attacks. From authentication to input validation to rate limiting and more.

B
Bootspring Team
Engineering
August 20, 2023
5 min read

APIs are the front door to your data. A single vulnerability can expose user data, enable account takeover, or bring down your service. Here's how to secure them.

Authentication#

1// Use strong password hashing 2import bcrypt from 'bcrypt'; 3 4const SALT_ROUNDS = 12; 5 6async function hashPassword(password: string): Promise<string> { 7 return bcrypt.hash(password, SALT_ROUNDS); 8} 9 10// Implement proper JWT handling 11import jwt from 'jsonwebtoken'; 12 13function createToken(user: User): string { 14 return jwt.sign( 15 { 16 sub: user.id, 17 role: user.role, 18 // Don't include sensitive data 19 }, 20 process.env.JWT_SECRET!, 21 { 22 expiresIn: '15m', 23 issuer: 'your-app', 24 audience: 'your-app', 25 } 26 ); 27} 28 29// Verify tokens properly 30function verifyToken(token: string): JWTPayload { 31 return jwt.verify(token, process.env.JWT_SECRET!, { 32 issuer: 'your-app', 33 audience: 'your-app', 34 }) as JWTPayload; 35}

Input Validation#

1import { z } from 'zod'; 2 3// Validate all inputs 4const createUserSchema = z.object({ 5 email: z.string().email().max(255), 6 password: z.string().min(8).max(128), 7 name: z.string().min(1).max(100), 8}); 9 10// Sanitize user input 11import DOMPurify from 'isomorphic-dompurify'; 12 13function sanitizeHtml(input: string): string { 14 return DOMPurify.sanitize(input, { 15 ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'], 16 ALLOWED_ATTR: [], 17 }); 18} 19 20// Prevent SQL injection - use parameterized queries 21// ❌ Never do this 22const bad = `SELECT * FROM users WHERE email = '${email}'`; 23 24// ✅ Always use parameters 25const good = await db.query( 26 'SELECT * FROM users WHERE email = $1', 27 [email] 28);

Rate Limiting#

1import rateLimit from 'express-rate-limit'; 2import RedisStore from 'rate-limit-redis'; 3 4// General API rate limit 5const apiLimiter = rateLimit({ 6 windowMs: 15 * 60 * 1000, // 15 minutes 7 max: 100, 8 standardHeaders: true, 9 store: new RedisStore({ client: redis }), 10}); 11 12// Strict limit for auth endpoints 13const authLimiter = rateLimit({ 14 windowMs: 60 * 60 * 1000, // 1 hour 15 max: 5, 16 skipSuccessfulRequests: true, 17 message: { error: 'Too many login attempts' }, 18}); 19 20app.use('/api/', apiLimiter); 21app.use('/api/auth/', authLimiter);

CORS Configuration#

1import cors from 'cors'; 2 3// Don't use cors() with no options in production! 4const corsOptions: cors.CorsOptions = { 5 origin: (origin, callback) => { 6 const allowedOrigins = [ 7 'https://yourapp.com', 8 'https://www.yourapp.com', 9 ]; 10 11 if (!origin || allowedOrigins.includes(origin)) { 12 callback(null, true); 13 } else { 14 callback(new Error('Not allowed by CORS')); 15 } 16 }, 17 methods: ['GET', 'POST', 'PUT', 'DELETE'], 18 allowedHeaders: ['Content-Type', 'Authorization'], 19 credentials: true, 20 maxAge: 86400, // 24 hours 21}; 22 23app.use(cors(corsOptions));

Security Headers#

1import helmet from 'helmet'; 2 3app.use(helmet({ 4 contentSecurityPolicy: { 5 directives: { 6 defaultSrc: ["'self'"], 7 scriptSrc: ["'self'"], 8 styleSrc: ["'self'", "'unsafe-inline'"], 9 imgSrc: ["'self'", 'data:', 'https:'], 10 connectSrc: ["'self'", 'https://api.yourapp.com'], 11 }, 12 }, 13 hsts: { 14 maxAge: 31536000, 15 includeSubDomains: true, 16 preload: true, 17 }, 18})); 19 20// Additional headers 21app.use((req, res, next) => { 22 res.setHeader('X-Content-Type-Options', 'nosniff'); 23 res.setHeader('X-Frame-Options', 'DENY'); 24 res.setHeader('X-XSS-Protection', '1; mode=block'); 25 res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); 26 next(); 27});

Authorization#

1// Check permissions on every request 2function requirePermission(permission: string) { 3 return async (req: Request, res: Response, next: NextFunction) => { 4 const user = req.user; 5 6 if (!user) { 7 return res.status(401).json({ error: 'Unauthorized' }); 8 } 9 10 const hasPermission = await checkPermission(user.id, permission); 11 12 if (!hasPermission) { 13 return res.status(403).json({ error: 'Forbidden' }); 14 } 15 16 next(); 17 }; 18} 19 20// Always verify resource ownership 21app.delete('/api/posts/:id', authenticate, async (req, res) => { 22 const post = await prisma.post.findUnique({ 23 where: { id: req.params.id }, 24 }); 25 26 // Check ownership! 27 if (!post || post.userId !== req.user.id) { 28 return res.status(404).json({ error: 'Not found' }); 29 } 30 31 await prisma.post.delete({ where: { id: req.params.id } }); 32 res.status(204).send(); 33});

Sensitive Data Protection#

1// Never return sensitive fields 2function sanitizeUser(user: User): SafeUser { 3 const { password, resetToken, ...safe } = user; 4 return safe; 5} 6 7// Mask sensitive data in logs 8function maskSensitiveData(data: any): any { 9 const sensitiveFields = ['password', 'token', 'secret', 'creditCard']; 10 const masked = { ...data }; 11 12 for (const field of sensitiveFields) { 13 if (masked[field]) { 14 masked[field] = '***REDACTED***'; 15 } 16 } 17 18 return masked; 19} 20 21// Use environment variables 22// ❌ Never hardcode secrets 23const secret = 'hardcoded-secret'; 24 25// ✅ Use environment variables 26const secret = process.env.JWT_SECRET; 27if (!secret) throw new Error('JWT_SECRET required');

Error Handling#

1// Don't leak internal errors 2app.use((error: Error, req: Request, res: Response, next: NextFunction) => { 3 // Log full error internally 4 logger.error({ 5 error: error.message, 6 stack: error.stack, 7 path: req.path, 8 }); 9 10 // Return safe error to client 11 if (error instanceof AppError) { 12 return res.status(error.statusCode).json({ 13 error: error.message, 14 code: error.code, 15 }); 16 } 17 18 // Generic error for unexpected issues 19 res.status(500).json({ 20 error: 'An unexpected error occurred', 21 code: 'INTERNAL_ERROR', 22 }); 23});

Security Checklist#

1## Authentication 2- [ ] Strong password hashing (bcrypt, Argon2) 3- [ ] Short-lived access tokens 4- [ ] Secure refresh token rotation 5- [ ] Account lockout after failed attempts 6- [ ] Secure password reset flow 7 8## Authorization 9- [ ] Check permissions on every request 10- [ ] Verify resource ownership 11- [ ] Use principle of least privilege 12- [ ] Audit sensitive operations 13 14## Input/Output 15- [ ] Validate all inputs with schema 16- [ ] Sanitize user-provided content 17- [ ] Use parameterized queries 18- [ ] Never return sensitive fields 19- [ ] Mask data in logs 20 21## Transport 22- [ ] HTTPS only 23- [ ] Secure cookies (HttpOnly, Secure, SameSite) 24- [ ] Proper CORS configuration 25- [ ] Security headers (CSP, HSTS) 26 27## Rate Limiting 28- [ ] API rate limits 29- [ ] Stricter auth endpoint limits 30- [ ] Per-user rate limits 31 32## Monitoring 33- [ ] Log security events 34- [ ] Alert on anomalies 35- [ ] Audit trail for sensitive actions

Conclusion#

API security requires defense in depth—multiple layers of protection. Validate inputs, authenticate requests, authorize access, rate limit, and monitor.

Security is not a feature you add—it's a practice you maintain.

Share this article

Help spread the word about Bootspring