API Security Best Practices Checklist

Meta Description: Secure your REST API with this comprehensive checklist covering authentication, authorization, encryption, rate limiting, input validation, and more. Keywords: api security, security best practices, api security checklist, secure api design, api hardening Word Count: ~2,300 words Your API is live. But is it secure? Security isn't one

TRY NANO BANANA FOR FREE

API Security Best Practices Checklist

TRY NANO BANANA FOR FREE
Contents

Meta Description: Secure your REST API with this comprehensive checklist covering authentication, authorization, encryption, rate limiting, input validation, and more.

Keywords: api security, security best practices, api security checklist, secure api design, api hardening

Word Count: ~2,300 words


Your API is live. But is it secure?

Security isn't one feature—it's a collection of practices. Miss one, and attackers find a way in.

Here's a complete security checklist for REST APIs.

Authentication & Authorization

✓ Use HTTPS Everywhere

Never use HTTP for APIs. Always use HTTPS (TLS 1.2 or higher).

// Redirect HTTP to HTTPS
app.use((req, res, next) => {
  if (req.protocol !== 'https' && process.env.NODE_ENV === 'production') {
    return res.redirect(301, `https://${req.hostname}${req.url}`);
  }
  next();
});

✓ Implement Proper Authentication

Choose the right method: - API Keys: Server-to-server - OAuth 2.0: User authorization - JWT: Stateless authentication

Never accept credentials in URLs:

❌ GET /api/pets?api_key=abc123
✓ GET /api/pets
   Authorization: Bearer abc123

✓ Verify Object-Level Authorization

Always check if the user owns or has permission to access the resource:

app.get('/v1/pets/:id', async (req, res) => {
  const pet = await db.pets.findById(req.params.id);

  if (!pet) {
    return res.status(404).json({ error: 'Not found' });
  }

  // Check ownership
  if (pet.ownerId !== req.auth.userId && !req.auth.isAdmin) {
    return res.status(403).json({ error: 'Access denied' });
  }

  res.json(pet);
});

✓ Implement Function-Level Authorization

Protect admin and privileged endpoints:

function requireAdmin(req, res, next) {
  if (!req.auth.scopes.includes('admin:all')) {
    return res.status(403).json({ error: 'Admin access required' });
  }
  next();
}

app.delete('/v1/admin/users/:id', requireAdmin, async (req, res) => {
  // Only admins can delete users
});

✓ Use Short-Lived Tokens

Tokens should expire:

const token = jwt.sign(
  { userId: user.id },
  JWT_SECRET,
  { expiresIn: '1h' } // Expire after 1 hour
);

Provide refresh tokens for long-lived sessions:

const accessToken = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId: user.id }, REFRESH_SECRET, { expiresIn: '7d' });

Input Validation

✓ Validate All Input

Never trust client input. Validate everything:

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

app.post('/v1/pets',
  body('name').isString().trim().isLength({ min: 1, max: 100 }),
  body('species').isIn(['DOG', 'CAT', 'BIRD', 'RABBIT']),
  body('age').optional().isInt({ min: 0, max: 30 }),
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.array() });
    }

    // Process valid input
  }
);

✓ Sanitize Input

Remove dangerous characters:

const sanitizeHtml = require('sanitize-html');

function sanitizeInput(input) {
  if (typeof input === 'string') {
    return sanitizeHtml(input, {
      allowedTags: [], // Strip all HTML
      allowedAttributes: {}
    });
  }
  return input;
}

✓ Prevent SQL Injection

Use parameterized queries:

// ❌ Vulnerable to SQL injection
const pets = await db.query(`SELECT * FROM pets WHERE name = '${req.query.name}'`);

// ✓ Safe with parameterized query
const pets = await db.query('SELECT * FROM pets WHERE name = ?', [req.query.name]);

✓ Prevent NoSQL Injection

Validate types in NoSQL queries:

// ❌ Vulnerable
const user = await db.users.findOne({ email: req.body.email });

// ✓ Safe
const user = await db.users.findOne({
  email: String(req.body.email) // Ensure it's a string
});

✓ Limit Request Size

Prevent large payload attacks:

app.use(express.json({ limit: '1mb' })); // Max 1MB request body
app.use(express.urlencoded({ limit: '1mb', extended: true }));

Rate Limiting

✓ Implement Rate Limiting

Protect against brute force and DoS:

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

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
  message: { error: 'Too many requests' }
});

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

✓ Stricter Limits for Sensitive Endpoints

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5, // Only 5 login attempts per 15 minutes
  skipSuccessfulRequests: true
});

app.post('/v1/auth/login', authLimiter, async (req, res) => {
  // Handle login
});

✓ Return Rate Limit Headers

app.use((req, res, next) => {
  res.setHeader('X-RateLimit-Limit', '100');
  res.setHeader('X-RateLimit-Remaining', '95');
  res.setHeader('X-RateLimit-Reset', '1678886400');
  next();
});

Data Protection

✓ Never Log Sensitive Data

// ❌ Don't log passwords, tokens, or PII
console.log('Login attempt:', req.body); // Contains password!

// ✓ Log safely
console.log('Login attempt:', { email: req.body.email });

✓ Hash Passwords

Never store plain text passwords:

const bcrypt = require('bcrypt');

async function hashPassword(password) {
  return await bcrypt.hash(password, 12); // 12 rounds
}

async function verifyPassword(password, hash) {
  return await bcrypt.compare(password, hash);
}

✓ Encrypt Sensitive Data at Rest

const crypto = require('crypto');

function encrypt(text, key) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
  const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
  const tag = cipher.getAuthTag();
  return { encrypted, iv, tag };
}

function decrypt(encrypted, iv, tag, key) {
  const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
  decipher.setAuthTag(tag);
  return decipher.update(encrypted) + decipher.final('utf8');
}

✓ Filter Sensitive Fields from Responses

function sanitizeUser(user) {
  const { passwordHash, ssn, creditCard, ...safeUser } = user;
  return safeUser;
}

app.get('/v1/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id);
  res.json(sanitizeUser(user));
});

Error Handling

✓ Don't Expose Stack Traces

// ❌ Exposes internal details
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.stack });
});

// ✓ Generic error message
app.use((err, req, res, next) => {
  console.error(err); // Log internally
  res.status(500).json({
    type: 'https://api.petstoreapi.com/errors/internal-error',
    title: 'Internal Server Error',
    status: 500,
    detail: 'An unexpected error occurred'
  });
});

✓ Use Standard Error Format

Follow RFC 9457:

function errorResponse(type, title, status, detail) {
  return {
    type: `https://api.petstoreapi.com/errors/${type}`,
    title,
    status,
    detail
  };
}

Headers & CORS

✓ Set Security Headers

const helmet = require('helmet');

app.use(helmet()); // Sets multiple security headers

// 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();
});

✓ Configure CORS Properly

const cors = require('cors');

app.use(cors({
  origin: ['https://myapp.com'], // Specific origins, not '*'
  credentials: true,
  maxAge: 86400
}));

Monitoring & Logging

✓ Log Security Events

function logSecurityEvent(event, details) {
  console.log(JSON.stringify({
    timestamp: new Date().toISOString(),
    event,
    ...details
  }));
}

// Log failed auth attempts
app.post('/v1/auth/login', async (req, res) => {
  const user = await authenticateUser(req.body);

  if (!user) {
    logSecurityEvent('failed_login', {
      email: req.body.email,
      ip: req.ip
    });
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Success
});

✓ Monitor for Anomalies

Track: - Failed authentication attempts - Rate limit violations - Unusual access patterns - Privilege escalation attempts

✓ Implement Audit Logs

async function auditLog(action, userId, resource, details) {
  await db.auditLogs.create({
    timestamp: new Date(),
    action,
    userId,
    resource,
    details,
    ip: req.ip
  });
}

app.delete('/v1/pets/:id', async (req, res) => {
  await deletePet(req.params.id);

  await auditLog('delete_pet', req.auth.userId, `pet:${req.params.id}`, {
    petName: pet.name
  });

  res.status(204).send();
});

Dependencies & Updates

✓ Keep Dependencies Updated

npm audit
npm audit fix

✓ Use Dependency Scanning

npm install -g snyk
snyk test

✓ Pin Dependency Versions

{
  "dependencies": {
    "express": "4.18.2", // Exact version, not "^4.18.2"
  }
}

Testing

✓ Test Security Controls

describe('Authorization', () => {
  it('should deny access to other users pets', async () => {
    const response = await request(app)
      .get('/v1/pets/123')
      .set('Authorization', `Bearer ${otherUserToken}`);

    expect(response.status).toBe(403);
  });

  it('should require authentication', async () => {
    const response = await request(app)
      .get('/v1/pets/123');

    expect(response.status).toBe(401);
  });
});

✓ Perform Penetration Testing

Regularly test for: - SQL injection - XSS - CSRF - Authentication bypass - Authorization bypass

Deployment

✓ Use Environment Variables

// ❌ Don't hardcode secrets
const JWT_SECRET = 'my-secret-key';

// ✓ Use environment variables
const JWT_SECRET = process.env.JWT_SECRET;

if (!JWT_SECRET) {
  throw new Error('JWT_SECRET environment variable is required');
}

✓ Disable Debug Mode in Production

if (process.env.NODE_ENV === 'production') {
  app.set('env', 'production');
  app.disable('x-powered-by');
}

✓ Use a WAF (Web Application Firewall)

Deploy behind a WAF like: - AWS WAF - Cloudflare - Akamai

Security Checklist Summary

Authentication & Authorization- [ ] HTTPS everywhere - [ ] Proper authentication method - [ ] Object-level authorization - [ ] Function-level authorization - [ ] Short-lived tokens

Input Validation- [ ] Validate all input - [ ] Sanitize input - [ ] Prevent SQL injection - [ ] Prevent NoSQL injection - [ ] Limit request size

Rate Limiting- [ ] Global rate limits - [ ] Endpoint-specific limits - [ ] Rate limit headers

Data Protection- [ ] No sensitive data in logs - [ ] Hash passwords - [ ] Encrypt sensitive data - [ ] Filter response fields

Error Handling- [ ] No stack traces exposed - [ ] Standard error format

Headers & CORS- [ ] Security headers - [ ] Proper CORS configuration

Monitoring- [ ] Log security events - [ ] Monitor anomalies - [ ] Audit logs

Dependencies- [ ] Keep dependencies updated - [ ] Dependency scanning - [ ] Pin versions

Testing- [ ] Security tests - [ ] Penetration testing

Deployment- [ ] Environment variables - [ ] Disable debug mode - [ ] Use WAF

Security is ongoing. Review this checklist regularly and stay updated on new vulnerabilities.


Related Articles: - OWASP Top 10 API Security- API Keys vs OAuth vs JWT- CORS Explained