Your API tests pass in CI. You deploy with confidence. Then /api/checkout starts timing out in production because the Stripe API is throttling you under real load — something no test environment could simulate.

API testing catches bugs before they ship. API monitoring catches bugs that testing can't. This guide covers both — with practical strategies you can implement this week.

The API Testing Pyramid

LayerWhat it testsWhen it runsSpeed
Contract testsResponse shape matches schemaEvery PRFast (ms)
Integration testsEndpoints work with real dependenciesEvery PRMedium (seconds)
Load testsPerformance under trafficBefore major releasesSlow (minutes)
Security testsOWASP API vulnerabilitiesWeekly or per releaseMedium
Production monitoringReal-world behaviorAlways (24/7)Continuous

Strategy 1: Contract Testing

Contract testing verifies that your API responses match the expected schema — the right fields, the right types, the right structure.

    // Example: Zod schema for contract testing in Next.js
import { z } from 'zod'

const UserResponseSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string(),
  createdAt: z.string().datetime(),
})

// In your test:
test('GET /api/users/:id returns valid user', async () => {
  const res = await fetch('/api/users/123')
  const data = await res.json()

  // This throws if the response doesn't match the schema
  const parsed = UserResponseSchema.parse(data)
  expect(parsed.email).toContain('@')
})

What it catches: Missing fields after a refactor, type changes (string → number), renamed properties, new required fields without defaults.

Strategy 2: Integration Testing

Integration tests hit your actual API endpoints with real (or realistic) database state.

    // Next.js API route integration test
import { createServer } from 'http'
import { NextApiHandler } from 'next'

test('POST /api/orders creates order and returns 201', async () => {
  const res = await fetch('http://localhost:3000/api/orders', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      items: [{ productId: 'prod_123', quantity: 2 }],
    }),
  })

  expect(res.status).toBe(201)
  const order = await res.json()
  expect(order.id).toBeDefined()
  expect(order.items).toHaveLength(1)
  expect(order.total).toBeGreaterThan(0)
})

What it catches: Database query bugs, missing validation, incorrect status codes, auth middleware issues.

Strategy 3: Load Testing

Load testing answers: "Can my API handle the traffic I expect?"

    # k6 load test for an API endpoint
import http from 'k6/http'
import { check, sleep } from 'k6'

export const options = {
  stages: [
    { duration: '30s', target: 50 },   // Ramp to 50 users
    { duration: '1m', target: 50 },     // Stay at 50
    { duration: '30s', target: 200 },   // Spike to 200
    { duration: '1m', target: 200 },    // Stay at 200
    { duration: '30s', target: 0 },     // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],    // P95 under 500ms
    http_req_failed: ['rate<0.01'],      // Error rate under 1%
  },
}

export default function () {
  const res = http.get('https://yourapi.com/api/products')
  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  })
  sleep(1)
}

What it catches: Connection pool exhaustion, memory leaks under sustained load, cascading failures, cold start bottlenecks on serverless.

Strategy 4: Security Testing

The OWASP API Security Top 10 covers the most common API vulnerabilities. Key tests:

  • Broken authentication: Can you access endpoints without a valid token?
  • Broken authorization: Can user A access user B's data by changing the ID?
  • Excessive data exposure: Does the response include fields it shouldn't (passwords, internal IDs)?
  • Rate limiting: Can you send 10,000 requests/second without being throttled?
  • Injection: Does the API sanitize input for SQL injection, NoSQL injection?
    // Simple auth test
test('GET /api/admin returns 401 without token', async () => {
  const res = await fetch('/api/admin')
  expect(res.status).toBe(401)
})

// Authorization test (BOLA — Broken Object Level Auth)
test('GET /api/users/:id cannot access other users', async () => {
  const res = await fetch('/api/users/other-user-id', {
    headers: { Authorization: 'Bearer user-a-token' },
  })
  expect(res.status).toBe(403)
})

Strategy 5: Production Monitoring

Testing catches bugs before deployment. Monitoring catches everything testing missed:

  • Intermittent failures — 2% of requests fail due to a race condition no test simulates
  • Performance degradation — P95 latency doubles because a database index was dropped
  • Third-party outages — Stripe is down, your checkout endpoint returns 500
  • Traffic patterns — 3 AM traffic spike from a new timezone you didn't anticipate

Nurbak Watch monitors every API route from inside your Next.js server — P50/P95/P99 latency, error rates, and throughput from real traffic. Alerts via Slack/WhatsApp in under 10 seconds.

    // instrumentation.ts — production monitoring in 5 lines
import { initWatch } from '@nurbak/watch'

export function register() {
  initWatch({
    apiKey: process.env.NURBAK_WATCH_KEY,
  })
}

Testing tells you it works. Monitoring tells you it's working right now. You need both.

Related Articles