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 actionsConclusion#
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.