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
| Layer | What it tests | When it runs | Speed |
|---|---|---|---|
| Contract tests | Response shape matches schema | Every PR | Fast (ms) |
| Integration tests | Endpoints work with real dependencies | Every PR | Medium (seconds) |
| Load tests | Performance under traffic | Before major releases | Slow (minutes) |
| Security tests | OWASP API vulnerabilities | Weekly or per release | Medium |
| Production monitoring | Real-world behavior | Always (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.

