API Security Best Practices: Protect Your REST APIs
Learn essential security best practices for securing your REST APIs against modern threats. From authentication to rate limiting.
Jean-Pierre Broeders
Freelance DevOps Engineer
API Security Best Practices: Protect Your REST APIs
APIs form the backbone of modern applications, but they're also a common attack vector. In this guide, I'll cover the most important security best practices you need to implement to protect your APIs.
1. Always Use HTTPS/TLS
This is the absolute foundation. Never run a production API over unencrypted HTTP.
# Nginx configuration: force HTTPS
server {
listen 80;
server_name api.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/api.example.com.crt;
ssl_certificate_key /etc/ssl/private/api.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
}
2. Implement Strong Authentication
JWT Tokens (Recommended)
JSON Web Tokens are ideal for stateless authentication:
// .NET Example: JWT Token Generation
public string GenerateJwtToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration["JwtSecret"]);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Email, user.Email)
}),
Expires = DateTime.UtcNow.AddHours(2),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature
)
};
return tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
}
API Keys for Service-to-Service
For machine-to-machine communication:
// Node.js: API Key validation middleware
const validateApiKey = async (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'API key missing' });
}
// Hash and compare
const hashedKey = crypto.createHash('sha256').update(apiKey).digest('hex');
const isValid = await db.apiKeys.findOne({ hash: hashedKey, active: true });
if (!isValid) {
return res.status(403).json({ error: 'Invalid API key' });
}
req.apiKeyId = isValid.id;
next();
};
3. Rate Limiting is Essential
Prevent brute force attacks and API abuse:
# Python/Flask: Simple rate limiter
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
@app.route("/api/sensitive")
@limiter.limit("5 per minute")
def sensitive_endpoint():
return {"data": "protected"}
For production: use Redis-backed rate limiting:
// Node.js + Redis
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const limiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:'
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // max 100 requests per window
message: 'Too many requests, please try again later'
});
app.use('/api/', limiter);
4. Input Validation & Sanitization
Never trust user input:
// TypeScript: Zod schema validation
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email(),
age: z.number().min(18).max(120),
username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/)
});
app.post('/api/users', async (req, res) => {
try {
const validated = userSchema.parse(req.body);
// Safe to use
await createUser(validated);
} catch (error) {
return res.status(400).json({ error: error.errors });
}
});
5. CORS Configuration
Restrict which origins can call your API:
// .NET Core: CORS policy
builder.Services.AddCors(options =>
{
options.AddPolicy("ProductionPolicy", builder =>
{
builder
.WithOrigins("https://app.example.com", "https://www.example.com")
.AllowedMethods("GET", "POST", "PUT", "DELETE")
.AllowedHeaders("Content-Type", "Authorization")
.AllowCredentials();
});
});
app.UseCors("ProductionPolicy");
6. Logging & Monitoring
Log security events, but never sensitive data:
// Good logging practice
logger.info('Login attempt', {
userId: user.id,
ip: req.ip,
success: true
// NO passwords, tokens, or PII!
});
// Alert on suspicious activity
if (failedAttempts > 5) {
await alertSecurityTeam({
type: 'BRUTE_FORCE_DETECTED',
ip: req.ip,
endpoint: '/api/login'
});
}
7. SQL Injection Prevention
Always use parameterized queries:
// ✅ GOOD: Parameterized query
var user = await db.Users
.FromSqlInterpolated($"SELECT * FROM Users WHERE Email = {email}")
.FirstOrDefaultAsync();
// ❌ BAD: String concatenation
var user = await db.Users
.FromSqlRaw($"SELECT * FROM Users WHERE Email = '{email}'")
.FirstOrDefaultAsync();
8. Versioning & Deprecation
Give API clients time to migrate:
GET /api/v1/users (deprecated, EOL 2026-06-01)
GET /api/v2/users (current)
Response headers:
res.setHeader('X-API-Version', 'v1');
res.setHeader('X-API-Deprecated', 'true');
res.setHeader('X-API-Sunset', '2026-06-01T00:00:00Z');
Production API Checklist
- [ ] HTTPS/TLS enforced
- [ ] Authentication (JWT/API Keys)
- [ ] Rate limiting implemented
- [ ] Input validation on all endpoints
- [ ] CORS properly configured
- [ ] Security headers (HSTS, CSP, etc.)
- [ ] Logging & monitoring active
- [ ] SQL injection prevention
- [ ] Secrets in environment variables
- [ ] API versioning strategy
Conclusion
API security isn't a "nice to have" but an absolute requirement. Implement these best practices from day one—adding them later is much harder and riskier.
Start with the basics (HTTPS, authentication, input validation) and build from there. Each of these measures significantly reduces your attack surface.
Pro tip: Use tools like OWASP ZAP or Burp Suite to test your own APIs for vulnerabilities. Better you than an attacker.
