API security is critical in 2026. With APIs handling sensitive data and business logic, a single vulnerability can expose your entire system. This guide covers essential security best practices every developer should implement.
API Security Checklist
| Category | Practice | Priority |
|---|---|---|
| Authentication | Use OAuth 2.0 or API keys | Critical |
| Authorization | Implement RBAC | Critical |
| Transport | HTTPS only | Critical |
| Input | Validate all inputs | Critical |
| Rate Limiting | Prevent abuse | High |
| Logging | Audit all requests | High |
| Error Handling | Don't leak info | High |
| CORS | Restrict origins | Medium |
1. Authentication Best Practices
API Keys
Simple but effective for server-to-server communication:
// Express middleware for API key validation
function validateApiKey(req, res, next) {
const apiKey = req.headers['x-api-key'] || req.query.api_key;
if (!apiKey) {
return res.status(401).json({
error: 'API key required',
code: 'MISSING_API_KEY'
});
}
// Use constant-time comparison to prevent timing attacks
const validKey = process.env.API_KEY;
const isValid = crypto.timingSafeEqual(
Buffer.from(apiKey),
Buffer.from(validKey)
);
if (!isValid) {
return res.status(401).json({
error: 'Invalid API key',
code: 'INVALID_API_KEY'
});
}
next();
}
app.use('/api', validateApiKey);
JWT Authentication
For user-based authentication:
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRY = '15m'; // Short-lived tokens
const REFRESH_EXPIRY = '7d';
function generateTokens(userId, role) {
const accessToken = jwt.sign(
{ userId, role, type: 'access' },
JWT_SECRET,
{ expiresIn: JWT_EXPIRY }
);
const refreshToken = jwt.sign(
{ userId, type: 'refresh' },
JWT_SECRET,
{ expiresIn: REFRESH_EXPIRY }
);
return { accessToken, refreshToken };
}
function verifyToken(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
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', code: 'TOKEN_EXPIRED' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
OAuth 2.0 Implementation
For third-party integrations:
const passport = require('passport');
const OAuth2Strategy = require('passport-oauth2');
passport.use('oauth2', new OAuth2Strategy({
authorizationURL: 'https://provider.com/oauth/authorize',
tokenURL: 'https://provider.com/oauth/token',
clientID: process.env.OAUTH_CLIENT_ID,
clientSecret: process.env.OAUTH_CLIENT_SECRET,
callbackURL: 'https://yourapi.com/auth/callback',
scope: ['read', 'write'],
state: true // CSRF protection
},
async (accessToken, refreshToken, profile, done) => {
// Store tokens securely
const user = await User.findOrCreate({
providerId: profile.id,
accessToken: encrypt(accessToken),
refreshToken: encrypt(refreshToken)
});
return done(null, user);
}
));
2. Authorization (RBAC)
Implement Role-Based Access Control:
// Define permissions
const permissions = {
admin: ['read', 'write', 'delete', 'manage_users'],
editor: ['read', 'write'],
viewer: ['read']
};
// Authorization middleware
function authorize(...requiredPermissions) {
return (req, res, next) => {
const userRole = req.user?.role;
if (!userRole) {
return res.status(401).json({ error: 'Authentication required' });
}
const userPermissions = permissions[userRole] || [];
const hasPermission = requiredPermissions.every(
perm => userPermissions.includes(perm)
);
if (!hasPermission) {
return res.status(403).json({
error: 'Insufficient permissions',
required: requiredPermissions,
userRole
});
}
next();
};
}
// Usage
app.get('/api/users', verifyToken, authorize('read'), getUsers);
app.post('/api/users', verifyToken, authorize('write', 'manage_users'), createUser);
app.delete('/api/users/:id', verifyToken, authorize('delete', 'manage_users'), deleteUser);
Resource-Level Authorization
Check ownership before allowing access:
async function authorizeResource(req, res, next) {
const { id } = req.params;
const userId = req.user.userId;
const resource = await Resource.findById(id);
if (!resource) {
return res.status(404).json({ error: 'Resource not found' });
}
// Check if user owns the resource or is admin
if (resource.ownerId !== userId && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
req.resource = resource;
next();
}
app.put('/api/posts/:id', verifyToken, authorizeResource, updatePost);
3. Input Validation
Never trust client input. Validate everything:
const Joi = require('joi');
// Define validation schemas
const schemas = {
createUser: Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).max(128).required(),
name: Joi.string().min(1).max(100).required(),
role: Joi.string().valid('viewer', 'editor').default('viewer')
}),
updateUser: Joi.object({
email: Joi.string().email(),
name: Joi.string().min(1).max(100)
}).min(1) // At least one field required
};
// Validation middleware
function validate(schemaName) {
return (req, res, next) => {
const schema = schemas[schemaName];
if (!schema) {
return res.status(500).json({ error: 'Validation schema not found' });
}
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true // Remove unknown fields
});
if (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.details.map(d => ({
field: d.path.join('.'),
message: d.message
}))
});
}
req.body = value; // Use sanitized data
next();
};
}
app.post('/api/users', validate('createUser'), createUser);
Prevent SQL Injection
Always use parameterized queries:
// BAD - SQL Injection vulnerable
const query = `SELECT * FROM users WHERE id = ${req.params.id}`;
// GOOD - Parameterized query
const query = 'SELECT * FROM users WHERE id = $1';
const result = await db.query(query, [req.params.id]);
// With an ORM (Prisma)
const user = await prisma.user.findUnique({
where: { id: parseInt(req.params.id) }
});
Prevent NoSQL Injection
// BAD - NoSQL Injection vulnerable
const user = await User.findOne({ username: req.body.username });
// GOOD - Validate and sanitize
const username = String(req.body.username).slice(0, 50);
const user = await User.findOne({ username });
// Or use mongo-sanitize
const sanitize = require('mongo-sanitize');
const user = await User.findOne({ username: sanitize(req.body.username) });
4. Rate Limiting
Protect against abuse and DDoS:
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
// General rate limiter
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
message: {
error: 'Too many requests',
retryAfter: '15 minutes'
}
});
// Strict limiter for auth endpoints
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 attempts per hour
skipSuccessfulRequests: true, // Only count failures
message: {
error: 'Too many login attempts',
retryAfter: '1 hour'
}
});
// API key-based rate limiting
const apiKeyLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 60, // 60 requests per minute
keyGenerator: (req) => req.headers['x-api-key'] || req.ip,
store: new RedisStore({
sendCommand: (...args) => redisClient.call(...args)
})
});
app.use('/api', generalLimiter);
app.use('/api/auth', authLimiter);
app.use('/api/v1', apiKeyLimiter);
Sliding Window Rate Limiter
More precise rate limiting:
const Redis = require('ioredis');
const redis = new Redis();
async function slidingWindowRateLimit(key, limit, windowSeconds) {
const now = Date.now();
const windowStart = now - (windowSeconds * 1000);
const multi = redis.multi();
// Remove old entries
multi.zremrangebyscore(key, 0, windowStart);
// Add current request
multi.zadd(key, now, `${now}-${Math.random()}`);
// Count requests in window
multi.zcard(key);
// Set expiry
multi.expire(key, windowSeconds);
const results = await multi.exec();
const requestCount = results[2][1];
return {
allowed: requestCount <= limit,
remaining: Math.max(0, limit - requestCount),
resetAt: new Date(now + windowSeconds * 1000)
};
}
// Middleware
async function rateLimitMiddleware(req, res, next) {
const key = `ratelimit:${req.headers['x-api-key'] || req.ip}`;
const { allowed, remaining, resetAt } = await slidingWindowRateLimit(key, 100, 60);
res.setHeader('X-RateLimit-Limit', 100);
res.setHeader('X-RateLimit-Remaining', remaining);
res.setHeader('X-RateLimit-Reset', resetAt.toISOString());
if (!allowed) {
return res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: resetAt.toISOString()
});
}
next();
}
5. HTTPS and Transport Security
Always enforce HTTPS:
const helmet = require('helmet');
// Security headers
app.use(helmet());
// Force 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();
});
// HSTS header
app.use(helmet.hsts({
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true
}));
Certificate Pinning (Mobile Apps)
// React Native with certificate pinning
import { fetch } from 'react-native-ssl-pinning';
const response = await fetch('https://api.yoursite.com/data', {
method: 'GET',
sslPinning: {
certs: ['cert1', 'cert2'] // Certificate names in assets
},
headers: {
'Authorization': `Bearer ${token}`
}
});
6. Error Handling
Don't leak sensitive information:
// Error handler middleware
app.use((err, req, res, next) => {
// Log full error internally
console.error({
error: err.message,
stack: err.stack,
path: req.path,
method: req.method,
userId: req.user?.userId
});
// Don't expose internal errors to clients
if (process.env.NODE_ENV === 'production') {
// Generic error for unknown errors
if (!err.statusCode) {
return res.status(500).json({
error: 'Internal server error',
requestId: req.id // For support reference
});
}
}
// Known errors with safe messages
res.status(err.statusCode || 500).json({
error: err.message,
code: err.code,
...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
});
});
// Custom error class
class ApiError extends Error {
constructor(message, statusCode, code) {
super(message);
this.statusCode = statusCode;
this.code = code;
}
}
// Usage
throw new ApiError('User not found', 404, 'USER_NOT_FOUND');
7. CORS Configuration
Restrict cross-origin requests:
const cors = require('cors');
const corsOptions = {
origin: (origin, callback) => {
const allowedOrigins = [
'https://yourapp.com',
'https://admin.yourapp.com'
];
// Allow requests with no origin (mobile apps, Postman)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
exposedHeaders: ['X-RateLimit-Remaining', 'X-RateLimit-Reset'],
maxAge: 86400 // Cache preflight for 24 hours
};
app.use(cors(corsOptions));
8. Logging and Monitoring
Comprehensive request logging:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Request logging middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info({
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration,
ip: req.ip,
userAgent: req.headers['user-agent'],
userId: req.user?.userId,
apiKey: req.headers['x-api-key']?.slice(0, 8) + '...'
});
// Alert on suspicious activity
if (res.statusCode === 401 || res.statusCode === 403) {
logger.warn({
type: 'AUTH_FAILURE',
ip: req.ip,
path: req.path,
timestamp: new Date().toISOString()
});
}
});
next();
});
9. API Versioning
Support multiple versions securely:
// URL versioning
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// Header versioning
app.use('/api', (req, res, next) => {
const version = req.headers['api-version'] || 'v1';
if (version === 'v1') {
return v1Router(req, res, next);
} else if (version === 'v2') {
return v2Router(req, res, next);
}
res.status(400).json({ error: 'Invalid API version' });
});
// Deprecation warnings
app.use('/api/v1', (req, res, next) => {
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', 'Sat, 31 Dec 2026 23:59:59 GMT');
res.setHeader('Link', '</api/v2>; rel="successor-version"');
next();
});
10. Security Headers
Essential headers for API security:
app.use((req, res, next) => {
// Prevent MIME type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// XSS protection
res.setHeader('X-XSS-Protection', '1; mode=block');
// Content Security Policy
res.setHeader('Content-Security-Policy', "default-src 'none'");
// Don't cache sensitive responses
res.setHeader('Cache-Control', 'no-store');
res.setHeader('Pragma', 'no-cache');
next();
});
Security Checklist Summary
## Before Deployment
- [ ] HTTPS enforced (no HTTP fallback)
- [ ] API keys/tokens not in URLs or logs
- [ ] Input validation on all endpoints
- [ ] SQL/NoSQL injection prevention
- [ ] Rate limiting configured
- [ ] CORS properly restricted
- [ ] Security headers set
- [ ] Error messages don't leak info
- [ ] Authentication on all protected routes
- [ ] Authorization (RBAC) implemented
- [ ] Secrets in environment variables
- [ ] Dependencies up to date
- [ ] Security logging enabled
- [ ] API versioning strategy
Conclusion
API security requires multiple layers of protection. Start with the critical items: authentication, authorization, HTTPS, and input validation. Then add rate limiting, logging, and proper error handling.
Regular security audits and keeping dependencies updated are equally important. Use tools like OWASP ZAP or Burp Suite to test your APIs for vulnerabilities.
Related Resources: