Back to Blog
Security10 min

API Security Checklist: 15 Essential Best Practices

Comprehensive guide to securing REST APIs with practical examples and proven security patterns.

By Security TeamFebruary 28, 2026

APIs are the backbone of modern applications, but they're also a prime target for attackers. A single API vulnerability can expose your entire system.

This checklist covers 15 essential security practices to protect your APIs from common attacks.

1. Use HTTPS Everywhere

Never allow unencrypted HTTP traffic for APIs.

// ❌ Bad: Allows HTTP
app.listen(3000);

// ✅ Good: HTTPS only + redirect HTTP to HTTPS
const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('private-key.pem'),
  cert: fs.readFileSync('certificate.pem')
};

https.createServer(options, app).listen(443);

// Redirect HTTP to HTTPS
app.use((req, res, next) => {
  if (!req.secure) {
    return res.redirect(`https://${req.headers.host}${req.url}`);
  }
  next();
});

2. Implement Proper Authentication

JWT (Recommended for Stateless APIs)

const jwt = require('jsonwebtoken');

// Generate token
const token = jwt.sign(
  { userId: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: '1h' }
);

// Verify token middleware
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

API Keys (For Service-to-Service)

// Generate secure API key
const crypto = require('crypto');
const apiKey = crypto.randomBytes(32).toString('hex');

// Validate API key
function validateApiKey(req, res, next) {
  const key = req.headers['x-api-key'];
  
  if (!key || !validApiKeys.has(key)) {
    return res.status(403).json({ error: 'Invalid API key' });
  }
  
  next();
}

3. Enforce Authorization

Authentication confirms identity. Authorization confirms permissions.

// Role-based access control (RBAC)
function requireRole(role) {
  return (req, res, next) => {
    if (req.user.role !== role) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

// Usage
app.delete('/users/:id', authenticate, requireRole('admin'), deleteUser);

// Resource-based authorization
async function canEditPost(req, res, next) {
  const post = await Post.findById(req.params.id);
  
  if (!post) {
    return res.status(404).json({ error: 'Post not found' });
  }
  
  if (post.authorId !== req.user.id && req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Cannot edit this post' });
  }
  
  req.post = post;
  next();
}

app.put('/posts/:id', authenticate, canEditPost, updatePost);

4. Rate Limiting

Prevent abuse and DDoS attacks.

const rateLimit = require('express-rate-limit');

// Global rate limit
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
  message: 'Too many requests, please try again later.',
  standardHeaders: true,
  legacyHeaders: false,
});

app.use('/api/', limiter);

// Stricter limits for sensitive endpoints
const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 5, // 5 attempts per hour
  skipSuccessfulRequests: true, // Don't count successful logins
});

app.post('/api/login', authLimiter, login);

5. Input Validation

Never trust client input. Validate everything.

const { body, validationResult } = require('express-validator');

app.post(
  '/users',
  [
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({ min: 8 }).matches(/\d/),
    body('age').optional().isInt({ min: 0, max: 120 }),
  ],
  (req, res) => {
    const errors = validationResult(req);
    
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    
    // Proceed with validated data
  }
);

6. Prevent SQL Injection

// ❌ Vulnerable
const query = `SELECT * FROM users WHERE email = '${email}'`;

// ✅ Safe: Parameterized queries
const query = 'SELECT * FROM users WHERE email = ?';
db.query(query, [email]);

// ✅ Safe: ORM
const user = await User.findOne({ where: { email } });

7. Use Proper HTTP Status Codes

// ✅ Clear and consistent
res.status(200).json({ data });          // Success
res.status(201).json({ created });       // Created
res.status(400).json({ error });         // Bad request
res.status(401).json({ error });         // Unauthorized
res.status(403).json({ error });         // Forbidden
res.status(404).json({ error });         // Not found
res.status(500).json({ error });         // Server error

8. Don't Expose Sensitive Data

// ❌ Bad: Exposes password hash and internal IDs
res.json({ user });

// ✅ Good: Serialize response
function serializeUser(user) {
  return {
    id: user.id,
    email: user.email,
    name: user.name,
    createdAt: user.createdAt,
    // password, internal IDs, etc. excluded
  };
}

res.json({ user: serializeUser(user) });

9. Prevent Mass Assignment

// ❌ Vulnerable: User can set isAdmin=true
app.put('/users/:id', async (req, res) => {
  await User.update(req.params.id, req.body);  // Dangerous!
});

// ✅ Safe: Whitelist allowed fields
const allowedFields = ['name', 'email', 'bio'];

app.put('/users/:id', async (req, res) => {
  const updates = {};
  for (const field of allowedFields) {
    if (req.body[field] !== undefined) {
      updates[field] = req.body[field];
    }
  }
  await User.update(req.params.id, updates);
});

10. Implement CORS Correctly

const cors = require('cors');

// ❌ Bad: Allows all origins
app.use(cors());

// ✅ Good: Whitelist specific origins
const allowedOrigins = ['https://myapp.com', 'https://admin.myapp.com'];

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
}));

11. Add Security Headers

const helmet = require('helmet');

app.use(helmet());

// Or manually:
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-XSS-Protection', '1; mode=block');
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  next();
});

12. Log Security Events

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'security.log' }),
  ],
});

// Log authentication failures
app.post('/login', async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  
  if (!user) {
    logger.warn('Failed login attempt', {
      email: req.body.email,
      ip: req.ip,
      userAgent: req.headers['user-agent'],
    });
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  logger.info('Successful login', { userId: user.id, ip: req.ip });
  // ...
});

13. Versioning

// URL versioning (recommended)
app.use('/api/v1', routesV1);
app.use('/api/v2', routesV2);

// Header versioning
app.use((req, res, next) => {
  const version = req.headers['api-version'] || '1';
  req.apiVersion = version;
  next();
});

14. Pagination and Limits

// ❌ Bad: Could return millions of records
app.get('/users', async (req, res) => {
  const users = await User.findAll();
  res.json(users);
});

// ✅ Good: Enforce pagination
app.get('/users', async (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = Math.min(parseInt(req.query.limit) || 20, 100); // Max 100
  const offset = (page - 1) * limit;
  
  const users = await User.findAll({ limit, offset });
  const total = await User.count();
  
  res.json({
    data: users,
    pagination: {
      page,
      limit,
      total,
      pages: Math.ceil(total / limit),
    },
  });
});

15. Monitor and Alert

  • Failed authentication attempts: Alert after 10 failures in 5 minutes
  • Unusual traffic patterns: Sudden spike in requests
  • Error rate spikes: High 5xx or 4xx responses
  • Slow endpoints: Response time > 1s
  • Security events: SQL injection attempts, XSS patterns
// Sentry integration
const Sentry = require('@sentry/node');

Sentry.init({ dsn: process.env.SENTRY_DSN });

app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.errorHandler());

API Security Checklist

  • ✅ HTTPS enforced, HTTP redirected
  • ✅ Authentication required for protected endpoints
  • ✅ Authorization checks on every request
  • ✅ Rate limiting implemented
  • ✅ Input validation on all endpoints
  • ✅ Parameterized queries (no SQL injection)
  • ✅ Proper HTTP status codes
  • ✅ Sensitive data excluded from responses
  • ✅ Mass assignment prevented
  • ✅ CORS configured correctly
  • ✅ Security headers set (helmet.js)
  • ✅ Security events logged
  • ✅ API versioning in place
  • ✅ Pagination enforced on list endpoints
  • ✅ Monitoring and alerting configured

Conclusion

API security is not optional. These 15 practices form a solid foundation for protecting your APIs. Implement them consistently across all endpoints.

Want a comprehensive security audit of your API? Get an API security audit and receive detailed vulnerability analysis within 24 hours.

APIsecurityRESTauthenticationauthorization

Ready to improve your code?

Get an AI-powered code audit with actionable recommendations. Results in 24 hours.

Start Your Audit