Comprehensive guide to securing REST and GraphQL APIs with practical examples and real-world scenarios

API Security Best Practices: Protecting Your Backend from Common Vulnerabilitiesh1
Hello! I’m Ahmet Zeybek, a full stack developer with extensive experience in building secure web applications. API security is not just a feature—it’s a fundamental requirement. In this comprehensive guide, I’ll share battle-tested security practices that have protected applications serving millions of users.
Why API Security Mattersh2
APIs are the backbone of modern applications, but they’re also prime targets for attackers. According to OWASP:
- 94% of applications have API vulnerabilities
- API attacks increased by 400% in recent years
- Average cost of API breach: $4.5 million
Authentication & Authorizationh2
1. JWT (JSON Web Tokens) Implementationh3
JWTs are industry standard but must be implemented correctly:
import jwt from 'jsonwebtoken'import bcrypt from 'bcrypt'import { randomBytes } from 'crypto'
// User authentication serviceclass AuthService { private jwtSecret: string private refreshTokenSecret: string
constructor() { this.jwtSecret = process.env.JWT_SECRET! this.refreshTokenSecret = process.env.REFRESH_TOKEN_SECRET! }
async authenticate(email: string, password: string): Promise<AuthTokens> { // Validate credentials const user = await this.userRepository.findByEmail(email) if (!user) { throw new AuthenticationError('Invalid credentials') }
const isValidPassword = await bcrypt.compare(password, user.passwordHash) if (!isValidPassword) { throw new AuthenticationError('Invalid credentials') }
// Generate tokens const accessToken = jwt.sign( { userId: user.id, email: user.email, role: user.role, type: 'access', }, this.jwtSecret, { expiresIn: '15m', // Short-lived access tokens issuer: 'myapp-api', audience: 'myapp-client', } )
const refreshToken = jwt.sign( { userId: user.id, type: 'refresh', }, this.refreshTokenSecret, { expiresIn: '7d', // Longer-lived refresh tokens issuer: 'myapp-api', audience: 'myapp-client', } )
// Store refresh token hash (not the token itself) const refreshTokenHash = await bcrypt.hash(refreshToken, 12) await this.tokenRepository.save({ userId: user.id, tokenHash: refreshTokenHash, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), })
return { accessToken, refreshToken } }
async refreshAccessToken(refreshToken: string): Promise<string> { try { // Verify refresh token const payload = jwt.verify(refreshToken, this.refreshTokenSecret) as any
if (payload.type !== 'refresh') { throw new Error('Invalid token type') }
// Check if refresh token exists in database const storedToken = await this.tokenRepository.findByUserId(payload.userId) if (!storedToken) { throw new Error('Refresh token not found') }
// Verify token hasn't expired if (new Date() > storedToken.expiresAt) { throw new Error('Refresh token expired') }
// Get user details const user = await this.userRepository.findById(payload.userId) if (!user) { throw new Error('User not found') }
// Generate new access token return jwt.sign( { userId: user.id, email: user.email, role: user.role, type: 'access', }, this.jwtSecret, { expiresIn: '15m', issuer: 'myapp-api', audience: 'myapp-client', } ) } catch (error) { throw new AuthenticationError('Invalid refresh token') } }}
// Middleware for protecting routesconst authenticateToken = async (req: Request, res: Response, next: NextFunction) => { const authHeader = req.headers['authorization'] const token = authHeader && authHeader.split(' ')[1] // Bearer TOKEN
if (!token) { return res.status(401).json({ error: 'Access token required' }) }
try { const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any
if (decoded.type !== 'access') { return res.status(401).json({ error: 'Invalid token type' }) }
req.user = decoded next() } catch (error) { if (error.name === 'TokenExpiredError') { return res.status(401).json({ error: 'Token expired' }) } return res.status(403).json({ error: 'Invalid token' }) }}
2. Role-Based Access Control (RBAC)h3
Implement granular permissions:
// Permission definitionsconst PERMISSIONS = { READ_USERS: 'read:users', WRITE_USERS: 'write:users', DELETE_USERS: 'delete:users', READ_ORDERS: 'read:orders', WRITE_ORDERS: 'write:orders', ADMIN_PANEL: 'access:admin',} as const
// Role definitionsconst ROLES = { CUSTOMER: { permissions: [PERMISSIONS.READ_ORDERS, PERMISSIONS.WRITE_ORDERS], }, MANAGER: { permissions: [PERMISSIONS.READ_USERS, PERMISSIONS.READ_ORDERS, PERMISSIONS.WRITE_ORDERS], }, ADMIN: { permissions: Object.values(PERMISSIONS), },}
// Authorization middlewareconst authorize = (permissions: string[]) => { return (req: Request, res: Response, next: NextFunction) => { const userPermissions = req.user?.permissions || []
const hasPermission = permissions.some((permission) => userPermissions.includes(permission))
if (!hasPermission) { return res.status(403).json({ error: 'Insufficient permissions' }) }
next() }}
// Usage in routesapp.get('/api/users', authenticateToken, authorize([PERMISSIONS.READ_USERS]), getUsers)app.post('/api/users', authenticateToken, authorize([PERMISSIONS.WRITE_USERS]), createUser)app.get('/api/admin', authenticateToken, authorize([PERMISSIONS.ADMIN_PANEL]), getAdminPanel)
Input Validation & Sanitizationh2
3. Request Validationh3
Never trust user input:
import { body, param, query, validationResult } from 'express-validator'import DOMPurify from 'isomorphic-dompurify'
// Validation middlewareconst validateUserCreation = [ body('email').isEmail().normalizeEmail().withMessage('Valid email is required'),
body('password') .isLength({ min: 8, max: 128 }) .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/) .withMessage('Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character'),
body('firstName') .trim() .isLength({ min: 1, max: 100 }) .matches(/^[a-zA-Z\s]+$/) .withMessage('First name must contain only letters and spaces'),
body('bio') .optional() .isLength({ max: 500 }) .customSanitizer((value) => { // Sanitize HTML content return DOMPurify.sanitize(value) }),
(req: Request, res: Response, next: NextFunction) => { const errors = validationResult(req) if (!errors.isEmpty()) { return res.status(400).json({ error: 'Validation failed', details: errors.array(), }) } next() },]
// SQL Injection preventionconst validateOrderQuery = [ query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1 and 100'),
query('offset').optional().isInt({ min: 0 }).withMessage('Offset must be non-negative'),
query('status').optional().isIn(['pending', 'processing', 'shipped', 'delivered', 'cancelled']).withMessage('Invalid status value'),]
4. File Upload Securityh3
Handle file uploads securely:
import multer from 'multer'import { extname, basename } from 'path'import { v4 as uuidv4 } from 'uuid'
// Configure multer with security settingsconst storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, './uploads/') }, filename: (req, file, cb) => { // Generate secure filename const ext = extname(file.originalname).toLowerCase() const name = basename(file.originalname, ext) const secureName = `${uuidv4()}-${name.replace(/[^a-zA-Z0-9]/g, '_')}${ext}` cb(null, secureName) },})
// File filter for securityconst fileFilter = (req: any, file: Express.Multer.File, cb: multer.FileFilterCallback) => { // Allowed file types const allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf']
// Check MIME type if (!allowedMimes.includes(file.mimetype)) { return cb(new Error('Invalid file type')) }
// Check file extension const allowedExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.pdf'] const ext = extname(file.originalname).toLowerCase()
if (!allowedExts.includes(ext)) { return cb(new Error('Invalid file extension')) }
// Check file size (5MB limit) if (file.size > 5 * 1024 * 1024) { return cb(new Error('File too large')) }
cb(null, true)}
const upload = multer({ storage, fileFilter, limits: { fileSize: 5 * 1024 * 1024, // 5MB files: 5, // Maximum 5 files per request },})
// Virus scanning middlewareconst scanForViruses = async (req: any, res: Response, next: NextFunction) => { if (!req.files || req.files.length === 0) { return next() }
try { // Integrate with virus scanning service (e.g., ClamAV, VirusTotal) for (const file of req.files) { const isClean = await virusScanner.scanFile(file.path) if (!isClean) { // Delete infected file fs.unlinkSync(file.path) return res.status(400).json({ error: 'Malicious file detected' }) } } next() } catch (error) { next(error) }}
Rate Limiting & DDoS Protectionh2
5. Rate Limiting Implementationh3
Prevent abuse with intelligent rate limiting:
import rateLimit from 'express-rate-limit'import RedisStore from 'rate-limit-redis'
// Different rate limits for different endpointsconst strictLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 requests per window message: { error: 'Too many requests from this IP, please try again later', }, standardHeaders: true, legacyHeaders: false, store: new RedisStore({ sendCommand: (...args: string[]) => redisClient.call(...args), }),})
const generalLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100, message: { error: 'Too many requests, please slow down', }, standardHeaders: true, legacyHeaders: false,})
// Apply rate limiting to routesapp.post('/api/auth/login', strictLimiter)app.post('/api/auth/register', strictLimiter)app.get('/api/public/data', generalLimiter)
// Custom rate limiting based on user tierconst tieredLimiter = (req: any, res: Response, next: NextFunction) => { const userTier = req.user?.tier || 'free'
const limits = { free: { windowMs: 60 * 1000, max: 10 }, premium: { windowMs: 60 * 1000, max: 100 }, enterprise: { windowMs: 60 * 1000, max: 1000 }, }
const limit = limits[userTier] // Apply rate limiting logic based on user tier next()}
CORS Configurationh2
6. Secure CORS Setuph3
Configure CORS properly to prevent unauthorized access:
import cors from 'cors'
// Production CORS configurationconst corsOptions = { origin: function (origin: any, callback: any) { // Allow requests from specific domains const allowedOrigins = [ 'https://myapp.com', 'https://www.myapp.com', 'https://admin.myapp.com', // Add more domains as needed ]
// Allow requests with no origin (mobile apps, Postman, etc.) if (!origin) return callback(null, true)
if (allowedOrigins.includes(origin)) { return callback(null, true) } else { return callback(new Error('Not allowed by CORS')) } }, credentials: true, // Allow cookies and authorization headers methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'Authorization', 'X-API-Key'], exposedHeaders: ['X-Total-Count', 'X-Rate-Limit-Remaining'], maxAge: 86400, // Cache preflight response for 24 hours optionsSuccessStatus: 200, // Some legacy browsers choke on 204}
app.use(cors(corsOptions))
// Handle preflight requestsapp.options('*', cors(corsOptions))
Data Protectionh2
7. Secure Data Handlingh3
Protect sensitive data appropriately:
// Password hashingconst hashPassword = async (password: string): Promise<string> => { const saltRounds = 12 return await bcrypt.hash(password, saltRounds)}
// Credit card tokenizationconst tokenizeCard = async (cardNumber: string, userId: string): Promise<string> => { // Remove all non-digits const cleaned = cardNumber.replace(/\D/g, '')
// Keep only last 4 digits visible const lastFour = cleaned.slice(-4) const masked = '*'.repeat(cleaned.length - 4) + lastFour
// Generate token for secure storage const token = uuidv4()
// Store in secure vault (not regular database) await vault.store({ token, userId, maskedCard: masked, encryptedCard: await encrypt(cleaned), // Use proper encryption })
return token}
// PII data encryption at restconst encryptSensitiveData = (data: string): string => { const algorithm = 'aes-256-gcm' const key = process.env.ENCRYPTION_KEY! const iv = randomBytes(16)
const cipher = createCipher(algorithm, key) cipher.setAAD(Buffer.from('additional-auth-data'))
let encrypted = cipher.update(data, 'utf8', 'hex') encrypted += cipher.final('hex')
const authTag = cipher.getAuthTag()
return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted}
const decryptSensitiveData = (encryptedData: string): string => { const [ivHex, authTagHex, encrypted] = encryptedData.split(':')
const algorithm = 'aes-256-gcm' const key = process.env.ENCRYPTION_KEY! const iv = Buffer.from(ivHex, 'hex') const authTag = Buffer.from(authTagHex, 'hex')
const decipher = createDecipher(algorithm, key) decipher.setAuthTag(authTag) decipher.setAAD(Buffer.from('additional-auth-data'))
let decrypted = decipher.update(encrypted, 'hex', 'utf8') decrypted += decipher.final('utf8')
return decrypted}
API Gateway Securityh2
8. API Gateway Implementationh3
Use an API gateway for centralized security:
class APIGateway { private services: Map<string, ServiceConfig> = new Map()
constructor() { this.setupSecurityMiddleware() }
private setupSecurityMiddleware() { // Request validation this.validateRequestFormat()
// API key validation this.validateAPIKey()
// Rate limiting this.applyRateLimiting()
// Authentication this.authenticateRequest()
// Authorization this.authorizeRequest()
// Input sanitization this.sanitizeInput()
// Logging and monitoring this.logRequest() }
private validateAPIKey() { // Check API key in header or query parameter const apiKey = req.headers['x-api-key'] || req.query.api_key
if (!apiKey) { throw new Error('API key required') }
// Validate API key against database const service = await this.validateAPIKeyInDB(apiKey)
// Attach service info to request req.service = service }
private authenticateRequest() { const authHeader = req.headers.authorization
if (authHeader?.startsWith('Bearer ')) { const token = authHeader.substring(7) req.user = this.validateJWT(token) } // Additional auth methods... }
private authorizeRequest() { const { user, service } = req
if (service.requiresAuth && !user) { throw new Error('Authentication required') }
if (user && !this.hasPermission(user, req.path, req.method)) { throw new Error('Insufficient permissions') } }}
Monitoring & Loggingh2
9. Security Monitoringh3
Implement comprehensive security monitoring:
class SecurityMonitor { async logSecurityEvent(event: SecurityEvent): Promise<void> { const logEntry = { timestamp: new Date(), event: event.type, severity: event.severity, source: event.source, details: event.details, userAgent: req.headers['user-agent'], ip: req.ip, userId: req.user?.id, endpoint: req.path, method: req.method, }
// Log to multiple destinations await Promise.all([this.logToDatabase(logEntry), this.logToSyslog(logEntry), this.sendToSIEM(logEntry), this.alertIfCritical(logEntry)]) }
private async alertIfCritical(logEntry: any): Promise<void> { if (logEntry.severity === 'CRITICAL') { await this.sendAlert({ subject: `Critical Security Event: ${logEntry.event}`, body: JSON.stringify(logEntry, null, 2), recipients: ['security@myapp.com', 'oncall@myapp.com'], }) } }
async detectBruteForce(): Promise<void> { // Monitor failed login attempts const recentFailures = await this.getRecentFailures()
for (const [ip, attempts] of recentFailures) { if (attempts > 10) { await this.blockIP(ip) await this.logSecurityEvent({ type: 'BRUTE_FORCE_ATTACK', severity: 'HIGH', source: ip, details: { attempts, blocked: true }, }) } } }
async detectAnomalies(): Promise<void> { // Monitor unusual patterns const anomalies = await this.analyzeRequestPatterns()
for (const anomaly of anomalies) { await this.logSecurityEvent({ type: 'ANOMALOUS_ACTIVITY', severity: 'MEDIUM', source: anomaly.source, details: anomaly.pattern, }) } }}
// Usage in middlewareapp.use(async (req, res, next) => { const startTime = Date.now()
res.on('finish', async () => { const duration = Date.now() - startTime
// Log all requests await securityMonitor.logRequest({ method: req.method, path: req.path, statusCode: res.statusCode, duration, ip: req.ip, userAgent: req.headers['user-agent'], })
// Check for suspicious patterns if (duration > 10000) { // Requests taking longer than 10 seconds await securityMonitor.logSecurityEvent({ type: 'SLOW_REQUEST', severity: 'MEDIUM', source: req.ip, details: { duration, path: req.path }, }) } })
next()})
HTTPS & Transport Securityh2
10. Secure Communicationh3
Ensure all communication is encrypted:
// HTTPS configuration for Expressimport https from 'https'import fs from 'fs'
const httpsOptions = { key: fs.readFileSync('/path/to/private-key.pem'), cert: fs.readFileSync('/path/to/certificate.pem'), ca: fs.readFileSync('/path/to/ca-bundle.pem'), // Security headers secureOptions: constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1, ciphers: ['ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES128-SHA256', 'ECDHE-RSA-AES256-SHA384'].join(':'), honorCipherOrder: true,}
const httpsServer = https.createServer(httpsOptions, app)
// HTTP to HTTPS redirectapp.use((req, res, next) => { if (req.header('x-forwarded-proto') !== 'https') { res.redirect(`https://${req.header('host')}${req.url}`) } else { next() }})
// Security headers middlewareapp.use((req, res, next) => { // Prevent clickjacking res.setHeader('X-Frame-Options', 'DENY')
// Prevent MIME type sniffing res.setHeader('X-Content-Type-Options', 'nosniff')
// Enable XSS protection res.setHeader('X-XSS-Protection', '1; mode=block')
// Referrer policy res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin')
// Content Security Policy res.setHeader( 'Content-Security-Policy', "default-src 'self'; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https:; " + "font-src 'self'; " + "connect-src 'self'" )
// HSTS (HTTP Strict Transport Security) res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
next()})
Conclusionh2
API security is an ongoing process, not a one-time implementation. The patterns I’ve shared form a solid foundation, but you must:
- Stay updated with OWASP Top 10 and security advisories
- Regular security audits and penetration testing
- Monitor and respond to security events
- Educate your team on security best practices
- Implement defense in depth - multiple security layers
Remember: Security is not a feature, it’s a requirement. Build it into your development process from day one.
What security challenges are you facing in your APIs? Which of these patterns have you implemented? Share your experiences in the comments!
Further Readingh2
This post reflects my experience as of October 2025. Security threats evolve rapidly, so always verify the latest best practices and compliance requirements for your industry.