HomeServicesAboutContact
Insights

Building Secure APIs for Enterprise Applications

March 2026 8 min read
Back to insights

API security is not a feature you bolt on after launch. It is a foundational architectural decision that shapes how your entire system handles trust, data flow, and failure modes. After building and securing APIs for dozens of enterprise clients, we have distilled the patterns that consistently prevent breaches without introducing unnecessary friction.

This guide covers the four pillars we apply to every enterprise API project: authentication architecture, rate limiting, input validation, and audit logging.

Authentication Architecture: Beyond Simple Tokens

The most common mistake we see in enterprise APIs is treating authentication as a single layer. A production API serving multiple clients, internal services, and third-party integrations needs a layered authentication strategy.

Use short-lived JWTs with refresh token rotation

Access tokens should expire within 15 minutes. Refresh tokens should be rotated on every use, invalidating the previous token immediately. This limits the blast radius of a compromised token and makes stolen credentials useless within a narrow window.

// Token configuration - keep access tokens short-lived
const tokenConfig = {
  accessToken: {
    expiresIn: '15m',
    algorithm: 'RS256'     // Asymmetric - verify without the signing key
  },
  refreshToken: {
    expiresIn: '7d',
    rotateOnUse: true,     // Issue new refresh token on each use
    reuseDetection: true   // Flag if old refresh token is reused
  }
};

The reuseDetection flag is critical. If a refresh token that has already been rotated is used again, it indicates a token theft. At that point, invalidate the entire token family and force re-authentication.

Implement API key scoping for service-to-service calls

Not every API consumer is a human user. Internal microservices and third-party integrations often need machine-to-machine authentication. Use scoped API keys with explicit permission grants rather than sharing service account credentials.

// Scope-based API key validation middleware
function validateApiKey(requiredScopes) {
  return async (req, res, next) => {
    const key = req.headers['x-api-key'];
    const client = await keyStore.lookup(key);

    if (!client || client.revoked) {
      return res.status(401).json({ error: 'Invalid API key' });
    }

    const hasScopes = requiredScopes.every(
      scope => client.scopes.includes(scope)
    );

    if (!hasScopes) {
      auditLog.warn('scope_violation', { client: client.id, requiredScopes });
      return res.status(403).json({ error: 'Insufficient permissions' });
    }

    req.client = client;
    next();
  };
}

Each API key should be tied to a specific client identity, have a defined set of scopes, and be independently revocable. Store keys hashed, never in plain text.

Rate Limiting: Protecting Availability Without Hurting Legitimate Users

Rate limiting is about more than preventing abuse. It is about ensuring fair resource allocation across all API consumers and protecting downstream services from cascade failures.

Implement tiered rate limits

Different endpoints have different cost profiles. A simple data lookup is cheap; a report generation endpoint that triggers database aggregations is expensive. Rate limits should reflect this reality.

// Tiered rate limit configuration
const rateLimits = {
  standard: {
    windowMs: 60 * 1000,     // 1 minute window
    max: 100,                 // 100 requests per minute
    keyGenerator: (req) => req.client?.id || req.ip
  },
  expensive: {
    windowMs: 60 * 1000,
    max: 10,                  // 10 requests per minute for heavy endpoints
    keyGenerator: (req) => req.client?.id || req.ip
  },
  authentication: {
    windowMs: 15 * 60 * 1000, // 15 minute window
    max: 5,                    // 5 attempts per 15 minutes
    keyGenerator: (req) => req.ip  // Always by IP, not by account
  }
};

Authentication endpoints deserve special attention. Rate limit them by IP address, not by account identifier, to prevent credential stuffing attacks that rotate through different usernames.

Return rate limit headers

Always communicate rate limit state to clients through standard headers: X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. Well-behaved clients will use these to self-throttle before hitting the limit, reducing the overall load on your rate limiting infrastructure.

Input Validation: Trust Nothing From the Wire

Every byte that arrives at your API is potentially hostile. Input validation is your first line of defence, and it needs to be both strict and consistent across every endpoint.

Validate at the schema level, not in business logic

Use schema validation libraries to define the shape of acceptable input before it reaches your business logic. This creates a clean separation between "is this structurally valid?" and "does this make business sense?"

// Schema-first validation with explicit constraints
const createOrderSchema = {
  type: 'object',
  required: ['items', 'shippingAddress'],
  additionalProperties: false,   // Reject unexpected fields
  properties: {
    items: {
      type: 'array',
      minItems: 1,
      maxItems: 100,             // Prevent payload bombs
      items: {
        type: 'object',
        required: ['productId', 'quantity'],
        properties: {
          productId: { type: 'string', pattern: '^[a-zA-Z0-9-]{1,64}$' },
          quantity: { type: 'integer', minimum: 1, maximum: 9999 }
        }
      }
    },
    shippingAddress: { $ref: '#/definitions/address' },
    notes: { type: 'string', maxLength: 500 }
  }
};

The additionalProperties: false directive is often overlooked but critically important. It prevents attackers from injecting unexpected fields that might be processed by downstream systems with different validation rules.

Sanitise output as well as input

Input validation stops malicious data from entering your system. Output encoding stops it from causing damage when it leaves. Always encode data appropriately for its output context - HTML, JSON, SQL, or shell commands. Never assume that stored data is safe because it was validated on the way in. Validation rules change, bugs happen, and data migrated from legacy systems may not meet current standards.

Audit Logging: Building a Forensic Trail

When a security incident occurs, the quality of your audit logs determines whether you can understand what happened and contain the damage. Treat audit logging as a first-class feature, not an afterthought.

Log authentication events comprehensively

Every authentication event - successful or failed - should generate an audit record. Include the client identity, IP address, user agent, timestamp, and outcome. For failed attempts, log the reason for failure without leaking information that could help an attacker.

// Structured audit log entry
function logAuthEvent(event) {
  const entry = {
    timestamp: new Date().toISOString(),
    eventType: event.type,        // 'login_success', 'login_failure', 'token_refresh'
    clientId: event.clientId,
    ip: event.ip,
    userAgent: event.userAgent,
    outcome: event.outcome,
    metadata: {
      method: event.method,       // 'password', 'api_key', 'oauth'
      failureReason: event.outcome === 'failure' ? event.reason : undefined,
      tokenFamily: event.tokenFamily
    }
  };

  // Write to append-only audit store - separate from application logs
  auditStore.append(entry);
}

Separate audit logs from application logs

Audit logs should go to a dedicated, append-only store with its own access controls and retention policy. Application developers should not have write access to the audit trail. This separation ensures that a compromised application cannot cover its tracks by modifying audit records.

The best audit logging systems are the ones you never need to read - but when you do need them, they contain everything required to reconstruct what happened, who did it, and when.

Set retention policies early

Define how long you keep audit data before you start generating it. Regulatory requirements vary, but most enterprise environments need at least 12 months of detailed logs and 7 years of summarised records. Build archival and rotation into your logging pipeline from day one rather than retrofitting it when your storage bill becomes a problem.

Putting It All Together

API security is not about implementing any single technique perfectly. It is about layering multiple defences so that a failure in one area does not compromise the entire system. Authentication prevents unauthorised access. Rate limiting prevents abuse. Input validation prevents injection attacks. Audit logging enables detection and response.

Each of these pillars reinforces the others. Rate limiting protects your authentication endpoints from brute force. Audit logs detect patterns that rate limiting alone cannot catch. Input validation prevents attacks that authenticated users might attempt. Together, they create a security posture that is genuinely resilient rather than superficially compliant.

If you are building APIs that handle sensitive data or serve enterprise clients, invest in these foundations early. The cost of retrofitting security into a production API is an order of magnitude higher than building it in from the start.

Back to insights