11 mins
API Security Best Practices: Protecting Your Backend from Common Vulnerabilities

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 service
class 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 routes
const 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 definitions
const 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 definitions
const 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 middleware
const 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 routes
app.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 middleware
const 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 prevention
const 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 settings
const 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 security
const 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 middleware
const 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 endpoints
const 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 routes
app.post('/api/auth/login', strictLimiter)
app.post('/api/auth/register', strictLimiter)
app.get('/api/public/data', generalLimiter)
// Custom rate limiting based on user tier
const 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 configuration
const 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 requests
app.options('*', cors(corsOptions))

Data Protectionh2

7. Secure Data Handlingh3

Protect sensitive data appropriately:

// Password hashing
const hashPassword = async (password: string): Promise<string> => {
const saltRounds = 12
return await bcrypt.hash(password, saltRounds)
}
// Credit card tokenization
const 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 rest
const 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 middleware
app.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 Express
import 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 redirect
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(`https://${req.header('host')}${req.url}`)
} else {
next()
}
})
// Security headers middleware
app.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:

  1. Stay updated with OWASP Top 10 and security advisories
  2. Regular security audits and penetration testing
  3. Monitor and respond to security events
  4. Educate your team on security best practices
  5. 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.