API Testing: The Complete Guide for Developers in 2026
Table of Contents
- 1. Introduction to API Testing
- 2. Types of API Tests
- 3. HTTP Methods Deep Dive
- 4. Request Headers and Authentication
- 5. Request/Response Body Formats
- 6. Status Codes and What They Mean
- 7. Tools for API Testing
- 8. Writing API Tests
- 9. API Mocking and Stubbing
- 10. Rate Limiting and Retries
- 11. API Documentation (OpenAPI/Swagger)
- 12. Common Mistakes and Debugging Tips
- 13. GraphQL Testing
- 14. WebSocket Testing Basics
- 15. Frequently Asked Questions
Try Our HTTP Request Tester
Test APIs directly in your browser with our free HTTP Request Tester. Send GET, POST, PUT, PATCH, and DELETE requests, set custom headers and authentication, and inspect full responses -- no installation required.
1. Introduction to API Testing
APIs are the connective tissue of modern software. Every time you log into an app with Google, check the weather on your phone, or pay for something online, dozens of API calls happen behind the scenes. API testing is the practice of sending requests to these interfaces and verifying that the responses are correct, fast, and secure.
Unlike UI testing, which interacts with buttons and forms, API testing operates at the protocol level. You send an HTTP request with a specific method, URL, headers, and body, then validate the response status code, headers, and payload. This makes API tests faster, more reliable, and easier to automate than end-to-end browser tests.
API testing matters because:
- APIs are contracts: Frontend teams, mobile teams, and third-party integrators all depend on your API behaving exactly as documented. A broken endpoint can cascade into failures across multiple applications.
- Bugs surface earlier: Testing at the API layer catches logic errors, data validation issues, and authentication problems before they reach the UI.
- Speed and stability: API tests execute in milliseconds, not minutes. A suite of 500 API tests can run in under 30 seconds, while the same coverage in UI tests might take an hour.
- Microservices demand it: In a distributed architecture, each service exposes APIs that must be tested independently and in combination.
- Security is non-negotiable: APIs are the primary attack surface for modern applications. Testing authentication, authorization, input validation, and rate limiting is essential.
Whether you are building a REST API, a GraphQL server, or a real-time WebSocket service, understanding how to test your APIs thoroughly is one of the most valuable skills a developer can have. This guide covers everything you need to know, from the fundamentals of HTTP methods to advanced topics like contract testing and API mocking.
2. Types of API Tests
Not all API tests serve the same purpose. A well-rounded testing strategy combines multiple types, each catching different categories of bugs at different stages of development.
Unit Tests
Unit tests verify that individual endpoints behave correctly in isolation. You test a single route handler or controller method, mocking any external dependencies like databases or third-party services.
// Example: Unit testing an Express route handler
const { getUser } = require('./handlers');
test('returns 404 when user not found', async () => {
const req = { params: { id: 'nonexistent' } };
const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
await getUser(req, res);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({ error: 'User not found' });
});
Unit tests are fast (no network calls), deterministic (no flaky external dependencies), and pinpoint exactly where failures occur. Every endpoint should have unit tests covering its happy path and error cases.
Integration Tests
Integration tests verify that multiple components work together correctly. You spin up the actual server, connect to a test database, and send real HTTP requests. These tests catch issues like incorrect database queries, misconfigured middleware, and serialization bugs.
// Example: Integration test with supertest
const request = require('supertest');
const app = require('./app');
test('POST /api/users creates a user and returns 201', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Alice', email: 'alice@example.com' })
.expect(201);
expect(response.body).toMatchObject({
name: 'Alice',
email: 'alice@example.com'
});
expect(response.body.id).toBeDefined();
});
Contract Tests
Contract tests verify that the API's request and response schemas have not changed unexpectedly. When your API is consumed by multiple clients (web app, mobile app, third-party integrators), a schema change can break all of them simultaneously. Contract testing tools like Pact let consumers define the response shape they expect, and producers verify they still match.
// Pact consumer test example
const { Pact } = require('@pact-foundation/pact');
const provider = new Pact({
consumer: 'WebApp',
provider: 'UserService'
});
describe('User API contract', () => {
it('returns user by ID', async () => {
await provider.addInteraction({
state: 'user with ID 1 exists',
uponReceiving: 'a request for user 1',
withRequest: {
method: 'GET',
path: '/api/users/1',
headers: { Accept: 'application/json' }
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: 1,
name: like('Alice'),
email: like('alice@example.com')
}
}
});
});
});
Load and Performance Tests
Load tests measure how your API performs under stress. You simulate hundreds or thousands of concurrent requests and measure response times, throughput, error rates, and resource consumption. Tools like k6, Artillery, and Apache JMeter are purpose-built for this.
// k6 load test script
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 50 }, // Ramp up to 50 users
{ duration: '1m', target: 50 }, // Stay at 50 users
{ duration: '30s', target: 200 }, // Spike to 200 users
{ duration: '1m', target: 200 }, // Stay at 200 users
{ duration: '30s', target: 0 }, // Ramp down
],
};
export default function () {
const res = http.get('https://api.example.com/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
Security Tests
Security tests probe your API for vulnerabilities. This includes testing for SQL injection, broken authentication, excessive data exposure, missing rate limiting, and insecure direct object references (IDOR). Tools like OWASP ZAP and Burp Suite automate many of these checks, but manual testing is equally important.
Key security tests to run on every API:
- Authentication bypass: Can you access protected endpoints without a token?
- Authorization escalation: Can user A access user B's data?
- Input injection: Does the API sanitize SQL, NoSQL, and command injection payloads?
- Data exposure: Does the API return sensitive fields (passwords, tokens, internal IDs) in responses?
- Rate limiting: Can an attacker send unlimited requests?
- CORS misconfiguration: Does the API accept requests from unauthorized origins?
3. HTTP Methods Deep Dive
HTTP methods (also called verbs) tell the server what action you want to perform on a resource. Understanding them is fundamental to both building and testing APIs.
GET -- Retrieve a Resource
GET requests retrieve data without modifying anything on the server. They are safe (no side effects) and idempotent (same request always returns the same result, assuming no other changes). GET requests should never have a request body.
# Fetch a list of users
curl -X GET https://api.example.com/users
# Fetch a single user
curl -X GET https://api.example.com/users/42
# Fetch with query parameters
curl -X GET "https://api.example.com/users?page=2&limit=10&sort=name"
When testing GET endpoints, verify: correct status codes (200 for success, 404 for missing resources), proper pagination, query parameter handling, and that sensitive data is not leaked in responses.
POST -- Create a Resource
POST requests create new resources. They are neither safe nor idempotent -- sending the same POST twice may create two resources. The request body contains the data for the new resource, and the response typically includes the created resource with its server-assigned ID.
# Create a new user
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'
# Expected response: 201 Created
# {
# "id": 42,
# "name": "Alice",
# "email": "alice@example.com",
# "createdAt": "2026-02-11T10:30:00Z"
# }
Test POST endpoints for: validation errors (missing required fields, invalid formats), duplicate handling, proper 201 status codes, Location header pointing to the new resource, and that the response body matches what was sent.
PUT -- Replace a Resource
PUT replaces the entire resource with the provided data. If you omit a field, it should be removed or set to its default value. PUT is idempotent -- sending the same PUT request multiple times produces the same result.
# Replace user 42 entirely
curl -X PUT https://api.example.com/users/42 \
-H "Content-Type: application/json" \
-d '{"name": "Alice Smith", "email": "alice.smith@example.com", "role": "admin"}'
A common testing mistake is sending a PUT with partial data and expecting it to work like PATCH. Always test that omitted fields are handled correctly (either rejected with 400 or set to defaults).
PATCH -- Partially Update a Resource
PATCH applies a partial modification to a resource. You send only the fields that need to change. This is more bandwidth-efficient than PUT and is the preferred method for most update operations.
# Update only the email
curl -X PATCH https://api.example.com/users/42 \
-H "Content-Type: application/json" \
-d '{"email": "newemail@example.com"}'
# JSON Patch format (RFC 6902)
curl -X PATCH https://api.example.com/users/42 \
-H "Content-Type: application/json-patch+json" \
-d '[{"op": "replace", "path": "/email", "value": "newemail@example.com"}]'
DELETE -- Remove a Resource
DELETE removes the specified resource. It is idempotent -- deleting the same resource twice should return 204 (or 404 the second time, depending on your design). Some APIs use soft deletes (marking records as deleted without removing them).
# Delete user 42
curl -X DELETE https://api.example.com/users/42
# Expected: 204 No Content (or 200 with body)
Test DELETE for: proper authorization (can only delete your own resources), cascade behavior (what happens to related data), and idempotency (second delete returns 404 or 204).
HEAD -- Get Headers Only
HEAD is identical to GET but returns only response headers, not the body. It is useful for checking if a resource exists, verifying content type, or reading cache headers without downloading the full response.
# Check if a resource exists
curl -I https://api.example.com/users/42
# HTTP/2 200
# Content-Type: application/json
# Content-Length: 256
# ETag: "abc123"
# Cache-Control: max-age=3600
OPTIONS -- Discover Allowed Methods
OPTIONS returns the HTTP methods that a resource supports. It is also used for CORS preflight requests, where the browser checks if a cross-origin request is permitted before sending the actual request.
# Check allowed methods
curl -X OPTIONS https://api.example.com/users
# HTTP/2 204
# Allow: GET, POST, OPTIONS
# Access-Control-Allow-Origin: https://myapp.com
# Access-Control-Allow-Methods: GET, POST, PUT, DELETE
# Access-Control-Allow-Headers: Content-Type, Authorization
| Method | Purpose | Safe | Idempotent | Has Body |
|---|---|---|---|---|
| GET | Retrieve resource | Yes | Yes | No |
| POST | Create resource | No | No | Yes |
| PUT | Replace resource | No | Yes | Yes |
| PATCH | Partial update | No | Depends | Yes |
| DELETE | Remove resource | No | Yes | Optional |
| HEAD | Headers only | Yes | Yes | No |
| OPTIONS | Discover methods | Yes | Yes | No |
Test HTTP Methods Instantly
Use our HTTP Request Tester to send GET, POST, PUT, PATCH, and DELETE requests to any API. Set custom headers, authentication, and request bodies, then inspect the full response with status codes, headers, and timing.
4. Request Headers and Authentication
HTTP headers carry metadata about the request and response. Some headers control content negotiation, others handle caching, and several are critical for authentication and security.
Essential Request Headers
# Content negotiation
Content-Type: application/json # What you're sending
Accept: application/json # What you want back
# Caching
If-None-Match: "etag-value" # Conditional GET
If-Modified-Since: Tue, 11 Feb 2026 10:00:00 GMT
# Client identification
User-Agent: MyApp/1.0
X-Request-ID: uuid-for-tracing
# CORS
Origin: https://myapp.com
Bearer Token Authentication
Bearer tokens are the most common authentication method for modern APIs. The client includes a token (usually a JWT) in the Authorization header. The server validates the token and extracts the user's identity from its claims.
# Bearer token in Authorization header
curl -X GET https://api.example.com/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# Test: What happens with expired token?
# Expected: 401 Unauthorized with error message
# Test: What happens with malformed token?
# Expected: 401 Unauthorized
# Test: What happens with no token?
# Expected: 401 Unauthorized
When testing Bearer auth, decode the JWT to understand its claims. Use our JWT Decoder to inspect token payloads, expiration times, and signatures. Need to create test tokens? Try our JWT Generator.
API Key Authentication
API keys are simple tokens that identify the calling application. They are typically sent as a header or query parameter. API keys are best for server-to-server communication where OAuth would be overkill.
# API key in header (preferred)
curl -X GET https://api.example.com/data \
-H "X-API-Key: sk_live_abc123def456"
# API key in query parameter (less secure -- visible in logs)
curl -X GET "https://api.example.com/data?api_key=sk_live_abc123def456"
Security tests for API keys: verify that invalid keys return 401 or 403, that keys cannot be used from unauthorized IP ranges, and that keys are not exposed in error messages or logs.
Basic Authentication
Basic Auth sends a base64-encoded username:password string in the Authorization header. It is simple but only secure over HTTPS, since base64 is encoding, not encryption.
# Basic Auth
curl -X GET https://api.example.com/data \
-u "username:password"
# This is equivalent to:
curl -X GET https://api.example.com/data \
-H "Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ="
OAuth 2.0
OAuth 2.0 is a delegation framework that lets users grant third-party apps limited access to their accounts without sharing passwords. It uses access tokens obtained through authorization flows.
# Step 1: Redirect user to authorization endpoint
GET https://auth.example.com/authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=https://yourapp.com/callback&
scope=read:users write:users&
state=random-csrf-token
# Step 2: Exchange authorization code for access token
curl -X POST https://auth.example.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE_FROM_CALLBACK" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "redirect_uri=https://yourapp.com/callback"
# Step 3: Use the access token
curl -X GET https://api.example.com/users/me \
-H "Authorization: Bearer ACCESS_TOKEN_HERE"
# Step 4: Refresh when expired
curl -X POST https://auth.example.com/token \
-d "grant_type=refresh_token" \
-d "refresh_token=REFRESH_TOKEN" \
-d "client_id=YOUR_CLIENT_ID"
Testing OAuth flows requires verifying: state parameter validation (CSRF protection), scope enforcement, token expiration and refresh, token revocation, and that authorization codes are single-use.
5. Request/Response Body Formats
The body of an HTTP request or response carries the actual data. Different formats serve different purposes, and the Content-Type header tells the server and client how to parse the body.
JSON (application/json)
JSON is the dominant format for modern APIs. It is lightweight, human-readable, and natively supported by JavaScript. Nearly every programming language has built-in JSON parsing.
// Request
POST /api/users HTTP/1.1
Content-Type: application/json
{
"name": "Alice",
"email": "alice@example.com",
"roles": ["admin", "editor"],
"preferences": {
"theme": "dark",
"language": "en"
}
}
// Response
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": 42,
"name": "Alice",
"email": "alice@example.com",
"roles": ["admin", "editor"],
"preferences": {
"theme": "dark",
"language": "en"
},
"createdAt": "2026-02-11T10:30:00Z"
}
When testing JSON APIs, validate the response structure using our JSON Validator and format complex responses with our JSON Formatter for easier reading.
XML (application/xml)
XML is common in legacy systems, SOAP APIs, and enterprise integrations. It is more verbose than JSON but supports schemas (XSD), namespaces, and attributes.
// Request
POST /api/users HTTP/1.1
Content-Type: application/xml
<user>
<name>Alice</name>
<email>alice@example.com</email>
<roles>
<role>admin</role>
<role>editor</role>
</roles>
</user>
Form Data (application/x-www-form-urlencoded)
URL-encoded form data is the format used by HTML forms. Keys and values are encoded into key=value pairs separated by ampersands. Special characters are percent-encoded.
# URL-encoded form data
curl -X POST https://api.example.com/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=alice&password=secret123&remember=true"
# The body is: username=alice&password=secret123&remember=true
Need to encode or decode URL parameters? Use our URL Encoder/Decoder to handle special characters correctly.
Multipart Form Data (multipart/form-data)
Multipart is used for file uploads and forms that combine text fields with binary data. Each part has its own Content-Type and content disposition.
# File upload with multipart
curl -X POST https://api.example.com/upload \
-F "file=@document.pdf" \
-F "description=Annual report" \
-F "category=finance"
# Multiple files
curl -X POST https://api.example.com/upload \
-F "files[]=@image1.png" \
-F "files[]=@image2.png" \
-F "album=vacation"
When testing multipart uploads, verify: file size limits, allowed MIME types, handling of malicious file names, and that the server correctly rejects files that exceed configured limits.
6. Status Codes and What They Mean
HTTP status codes tell the client whether a request succeeded, failed, or needs further action. They are grouped into five classes, each with a specific meaning.
2xx -- Success
| Code | Name | When to Use |
|---|---|---|
| 200 | OK | Standard success response for GET, PUT, PATCH |
| 201 | Created | Resource successfully created (POST). Include Location header. |
| 204 | No Content | Success with no response body (DELETE, PUT with no return) |
| 206 | Partial Content | Range request fulfilled (file download resume) |
3xx -- Redirection
| Code | Name | When to Use |
|---|---|---|
| 301 | Moved Permanently | Resource URL has changed permanently. Clients should update bookmarks. |
| 302 | Found | Temporary redirect. Often used in OAuth flows. |
| 304 | Not Modified | Resource hasn't changed since last request (caching). |
| 307 | Temporary Redirect | Like 302, but preserves the HTTP method. |
| 308 | Permanent Redirect | Like 301, but preserves the HTTP method. |
4xx -- Client Errors
| Code | Name | When to Use |
|---|---|---|
| 400 | Bad Request | Malformed request syntax, invalid JSON, missing required fields |
| 401 | Unauthorized | Missing or invalid authentication credentials |
| 403 | Forbidden | Authenticated but not authorized (insufficient permissions) |
| 404 | Not Found | Resource does not exist at this URL |
| 405 | Method Not Allowed | HTTP method not supported for this resource (e.g., DELETE on /users) |
| 409 | Conflict | Request conflicts with current state (duplicate email, version mismatch) |
| 422 | Unprocessable Entity | Valid JSON but semantic errors (invalid email format, age is negative) |
| 429 | Too Many Requests | Rate limit exceeded. Include Retry-After header. |
5xx -- Server Errors
| Code | Name | When to Use |
|---|---|---|
| 500 | Internal Server Error | Unexpected server failure. Never expose stack traces to clients. |
| 502 | Bad Gateway | Upstream server returned an invalid response |
| 503 | Service Unavailable | Server is temporarily down (maintenance, overload). Include Retry-After. |
| 504 | Gateway Timeout | Upstream server did not respond in time |
When testing, always verify that your API returns the correct status code for each scenario. A common mistake is returning 200 for everything and embedding the error in the response body. Proper status codes enable clients to handle errors programmatically without parsing the body.
For a deeper reference on all HTTP status codes, read our HTTP Status Codes Explained blog post.
7. Tools for API Testing
The right tool depends on your workflow. Here is a comparison of the most popular options in 2026.
curl -- The Command-Line Standard
curl is installed on virtually every Unix system and is the universal language for describing HTTP requests. Every developer should know the basics.
# GET request
curl https://api.example.com/users
# POST with JSON body
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"name": "Alice", "email": "alice@example.com"}'
# See response headers
curl -i https://api.example.com/users
# Verbose output (debug TLS, redirects, timing)
curl -v https://api.example.com/users
# Follow redirects
curl -L https://api.example.com/old-endpoint
# Save response to file
curl -o response.json https://api.example.com/users
# Upload a file
curl -X POST https://api.example.com/upload \
-F "file=@document.pdf"
# Measure response time
curl -o /dev/null -s -w "Time: %{time_total}s\n" https://api.example.com/users
Postman
Postman is the most popular GUI tool for API development and testing. It supports collections (groups of requests), environments (variable sets for dev/staging/prod), pre-request scripts, test assertions, and team collaboration. Its scripting capabilities make it powerful for complex testing workflows.
// Postman test script (runs after response)
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Response has correct structure", function () {
const json = pm.response.json();
pm.expect(json).to.have.property('id');
pm.expect(json).to.have.property('name');
pm.expect(json.email).to.include('@');
});
pm.test("Response time is under 500ms", function () {
pm.expect(pm.response.responseTime).to.be.below(500);
});
// Save token from login response to environment
const json = pm.response.json();
pm.environment.set("auth_token", json.token);
Insomnia
Insomnia is a lightweight alternative to Postman with a clean, developer-focused interface. It supports REST, GraphQL, gRPC, and WebSocket testing. Its environment chaining and plugin system make it highly customizable.
Thunder Client (VS Code)
Thunder Client brings API testing directly into VS Code. It is perfect for developers who prefer staying in their editor and do not need the full feature set of Postman. It supports collections, environments, and basic scripting.
DevToolbox HTTP Tester
For quick, no-install API testing, our HTTP Request Tester runs entirely in your browser. It supports all HTTP methods, custom headers, authentication, request bodies, and displays the full response including status, headers, body, and timing. No accounts, no downloads, no data sent to third parties.
| Tool | Type | Best For | Price |
|---|---|---|---|
| curl | CLI | Scripting, CI/CD, quick checks | Free |
| Postman | GUI | Team collaboration, complex workflows | Free / Paid |
| Insomnia | GUI | Lightweight testing, GraphQL | Free / Paid |
| Thunder Client | VS Code | In-editor testing, individual devs | Free / Paid |
| HTTP Tester | Browser | Quick tests, no install, privacy | Free |
8. Writing API Tests
Automated API tests belong in your CI/CD pipeline. They run on every commit, catching regressions before they reach production. Here is how to write them in the three most popular testing ecosystems.
JavaScript: Jest + Supertest
Supertest provides a fluent API for making HTTP requests against an Express app without starting a real server.
const request = require('supertest');
const app = require('../src/app');
const db = require('../src/db');
describe('Users API', () => {
let authToken;
beforeAll(async () => {
await db.migrate();
await db.seed();
// Get auth token
const loginRes = await request(app)
.post('/api/auth/login')
.send({ email: 'admin@test.com', password: 'password' });
authToken = loginRes.body.token;
});
afterAll(async () => {
await db.cleanup();
});
describe('GET /api/users', () => {
it('returns a list of users', async () => {
const res = await request(app)
.get('/api/users')
.set('Authorization', `Bearer ${authToken}`)
.expect('Content-Type', /json/)
.expect(200);
expect(res.body).toBeInstanceOf(Array);
expect(res.body.length).toBeGreaterThan(0);
expect(res.body[0]).toHaveProperty('id');
expect(res.body[0]).toHaveProperty('name');
expect(res.body[0]).toHaveProperty('email');
// Sensitive fields should NOT be exposed
expect(res.body[0]).not.toHaveProperty('password');
});
it('supports pagination', async () => {
const res = await request(app)
.get('/api/users?page=1&limit=5')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(res.body.length).toBeLessThanOrEqual(5);
});
it('returns 401 without authentication', async () => {
await request(app)
.get('/api/users')
.expect(401);
});
});
describe('POST /api/users', () => {
it('creates a new user', async () => {
const newUser = {
name: 'Test User',
email: 'test@example.com',
password: 'SecurePass123!'
};
const res = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${authToken}`)
.send(newUser)
.expect(201);
expect(res.body.name).toBe(newUser.name);
expect(res.body.email).toBe(newUser.email);
expect(res.body).not.toHaveProperty('password');
expect(res.headers.location).toMatch(/\/api\/users\/\d+/);
});
it('returns 400 for invalid email', async () => {
const res = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${authToken}`)
.send({ name: 'Test', email: 'not-an-email', password: 'pass' })
.expect(400);
expect(res.body.errors).toBeDefined();
});
it('returns 409 for duplicate email', async () => {
await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${authToken}`)
.send({ name: 'Dup', email: 'admin@test.com', password: 'pass' })
.expect(409);
});
});
});
Python: Pytest + Requests
import pytest
import requests
BASE_URL = "http://localhost:8000/api"
@pytest.fixture(scope="session")
def auth_headers():
"""Get authentication token."""
response = requests.post(f"{BASE_URL}/auth/login", json={
"email": "admin@test.com",
"password": "password"
})
token = response.json()["token"]
return {"Authorization": f"Bearer {token}"}
class TestUsersAPI:
def test_list_users(self, auth_headers):
response = requests.get(f"{BASE_URL}/users", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) > 0
assert "id" in data[0]
assert "name" in data[0]
assert "password" not in data[0] # No sensitive fields
def test_create_user(self, auth_headers):
new_user = {
"name": "Test User",
"email": "pytest-user@example.com",
"password": "SecurePass123!"
}
response = requests.post(
f"{BASE_URL}/users",
json=new_user,
headers=auth_headers
)
assert response.status_code == 201
data = response.json()
assert data["name"] == new_user["name"]
assert data["email"] == new_user["email"]
assert "password" not in data
def test_create_user_invalid_email(self, auth_headers):
response = requests.post(
f"{BASE_URL}/users",
json={"name": "Test", "email": "invalid", "password": "pass"},
headers=auth_headers
)
assert response.status_code == 400
assert "errors" in response.json()
def test_get_user_not_found(self, auth_headers):
response = requests.get(
f"{BASE_URL}/users/99999",
headers=auth_headers
)
assert response.status_code == 404
def test_unauthorized_access(self):
response = requests.get(f"{BASE_URL}/users")
assert response.status_code == 401
def test_response_time(self, auth_headers):
response = requests.get(f"{BASE_URL}/users", headers=auth_headers)
assert response.elapsed.total_seconds() < 1.0 # Under 1 second
Go: net/http/httptest
package api_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"myapp/api"
)
func TestGetUsers(t *testing.T) {
router := api.NewRouter()
req := httptest.NewRequest(http.MethodGet, "/api/users", nil)
req.Header.Set("Authorization", "Bearer "+getTestToken())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
var users []api.User
if err := json.Unmarshal(w.Body.Bytes(), &users); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if len(users) == 0 {
t.Error("expected at least one user")
}
for _, u := range users {
if u.Password != "" {
t.Error("password should not be in response")
}
}
}
func TestCreateUser(t *testing.T) {
router := api.NewRouter()
body := map[string]string{
"name": "Test User",
"email": "gotest@example.com",
"password": "SecurePass123!",
}
jsonBody, _ := json.Marshal(body)
req := httptest.NewRequest(http.MethodPost, "/api/users", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+getTestToken())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d", w.Code)
}
var user api.User
json.Unmarshal(w.Body.Bytes(), &user)
if user.Name != "Test User" {
t.Errorf("expected name 'Test User', got '%s'", user.Name)
}
location := w.Header().Get("Location")
if location == "" {
t.Error("expected Location header")
}
}
func TestUnauthorized(t *testing.T) {
router := api.NewRouter()
req := httptest.NewRequest(http.MethodGet, "/api/users", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Errorf("expected status 401, got %d", w.Code)
}
}
9. API Mocking and Stubbing
API mocking creates fake implementations of external APIs so your tests do not depend on real services. This eliminates network flakiness, reduces test execution time, and lets you test edge cases that are difficult to reproduce with real APIs (timeouts, 500 errors, rate limits).
Why Mock APIs?
- Speed: No network latency. Mock responses return in microseconds.
- Reliability: No flaky failures from external service outages.
- Edge cases: Simulate errors, slow responses, and malformed data on demand.
- Cost: Third-party APIs often charge per request. Mocking is free.
- Offline development: Work without internet or VPN access.
JavaScript: nock
nock intercepts HTTP requests at the Node.js level, returning predefined responses without hitting the network.
const nock = require('nock');
const { fetchUserProfile } = require('./userService');
describe('User Service', () => {
afterEach(() => {
nock.cleanAll();
});
it('fetches user profile from external API', async () => {
nock('https://external-api.com')
.get('/users/42')
.reply(200, {
id: 42,
name: 'Alice',
email: 'alice@example.com'
});
const user = await fetchUserProfile(42);
expect(user.name).toBe('Alice');
});
it('handles external API timeout', async () => {
nock('https://external-api.com')
.get('/users/42')
.delayConnection(5000) // Simulate 5s delay
.reply(200, {});
await expect(fetchUserProfile(42)).rejects.toThrow('timeout');
});
it('handles external API error', async () => {
nock('https://external-api.com')
.get('/users/42')
.reply(500, { error: 'Internal Server Error' });
await expect(fetchUserProfile(42)).rejects.toThrow('Server error');
});
it('handles rate limiting', async () => {
nock('https://external-api.com')
.get('/users/42')
.reply(429, { error: 'Too Many Requests' }, {
'Retry-After': '60'
});
await expect(fetchUserProfile(42)).rejects.toThrow('Rate limited');
});
});
Python: responses
import responses
import requests
@responses.activate
def test_fetch_user():
responses.add(
responses.GET,
"https://external-api.com/users/42",
json={"id": 42, "name": "Alice"},
status=200
)
response = requests.get("https://external-api.com/users/42")
assert response.json()["name"] == "Alice"
@responses.activate
def test_external_api_error():
responses.add(
responses.GET,
"https://external-api.com/users/42",
json={"error": "Internal Server Error"},
status=500
)
response = requests.get("https://external-api.com/users/42")
assert response.status_code == 500
Mock Servers
For more complex scenarios, standalone mock servers simulate entire APIs. Tools like WireMock, MockServer, and json-server let you define endpoints, response delays, and conditional logic.
# json-server: instant REST API from a JSON file
# Install: npm install -g json-server
# db.json
{
"users": [
{ "id": 1, "name": "Alice", "email": "alice@example.com" },
{ "id": 2, "name": "Bob", "email": "bob@example.com" }
],
"posts": [
{ "id": 1, "userId": 1, "title": "Hello World", "body": "..." }
]
}
# Start mock server
json-server --watch db.json --port 3001
# Now you have a full REST API:
# GET /users
# GET /users/1
# POST /users
# PUT /users/1
# PATCH /users/1
# DELETE /users/1
10. Rate Limiting and Retries
Rate limiting protects APIs from abuse and ensures fair usage across all clients. As a tester and consumer of APIs, you need to understand how rate limits work and how to handle them gracefully.
Common Rate Limiting Headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 100 # Max requests per window
X-RateLimit-Remaining: 42 # Requests remaining
X-RateLimit-Reset: 1707648000 # Unix timestamp when limit resets
# When rate limited:
HTTP/1.1 429 Too Many Requests
Retry-After: 60 # Wait 60 seconds before retrying
X-RateLimit-Remaining: 0
Implementing Exponential Backoff
When you hit a rate limit (or any transient error), retrying immediately will just fail again. Exponential backoff increases the delay between retries, reducing load on the server and improving your chances of success.
// JavaScript: Exponential backoff with jitter
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.pow(2, attempt) * 1000 + Math.random() * 1000;
console.log(`Rate limited. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
if (response.status >= 500 && attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
console.log(`Server error. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
return response;
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
# Python: Exponential backoff with tenacity
from tenacity import retry, stop_after_attempt, wait_exponential
import requests
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=1, min=1, max=60)
)
def call_api(url, headers=None):
response = requests.get(url, headers=headers)
if response.status_code == 429:
raise Exception("Rate limited")
if response.status_code >= 500:
raise Exception(f"Server error: {response.status_code}")
return response
Testing Rate Limit Handling
Your tests should verify that your application handles rate limits correctly:
- Does it read and respect the
Retry-Afterheader? - Does it implement backoff instead of hammering the server?
- Does it surface rate limit errors to users with helpful messages?
- Does it have a maximum retry count to prevent infinite loops?
- Does it log rate limit events for monitoring and alerting?
11. API Documentation (OpenAPI/Swagger)
The OpenAPI Specification (formerly Swagger) is the industry standard for describing REST APIs. It defines your endpoints, request/response schemas, authentication methods, and examples in a machine-readable format (YAML or JSON) that powers auto-generated documentation, client SDKs, and test scaffolding.
OpenAPI 3.1 Example
openapi: 3.1.0
info:
title: User Management API
version: 1.0.0
description: API for managing users and their profiles
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
summary: List all users
operationId: listUsers
tags: [Users]
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: List of users
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
'401':
$ref: '#/components/responses/Unauthorized'
post:
summary: Create a user
operationId: createUser
tags: [Users]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created
headers:
Location:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
$ref: '#/components/responses/BadRequest'
'409':
description: Email already exists
components:
schemas:
User:
type: object
required: [id, name, email]
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
createdAt:
type: string
format: date-time
CreateUserRequest:
type: object
required: [name, email, password]
properties:
name:
type: string
minLength: 1
maxLength: 100
email:
type: string
format: email
password:
type: string
minLength: 8
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
responses:
Unauthorized:
description: Missing or invalid authentication
BadRequest:
description: Invalid request body
Generating Tests from OpenAPI Specs
Tools like Schemathesis and Dredd can automatically generate API tests from your OpenAPI specification, testing every endpoint with valid and invalid data:
# Schemathesis: Fuzz test from OpenAPI spec
pip install schemathesis
# Run automated tests against your spec
schemathesis run https://api.example.com/openapi.json \
--hypothesis-max-examples=100 \
--checks all
# This will:
# - Test every endpoint with random valid data
# - Test with boundary values
# - Check for 500 errors
# - Verify response schemas match the spec
# - Check content-type headers
Swagger UI
Swagger UI renders your OpenAPI spec as interactive documentation where developers can read descriptions, see schemas, and send test requests directly from the browser. It is the fastest way to let consumers explore your API.
12. Common Mistakes and Debugging Tips
Years of building and testing APIs reveal the same mistakes recurring across teams. Avoiding these will save you hours of debugging.
Mistake 1: Only Testing the Happy Path
The most common sin in API testing. You test that POST /users works with valid data, but never test what happens with missing fields, invalid types, empty strings, extremely long strings, special characters, or SQL injection payloads.
Fix: For every endpoint, test at minimum: valid input, each required field missing, invalid data types, boundary values (empty string, max length, negative numbers, zero), and unauthorized access.
Mistake 2: Hardcoding URLs and Tokens
// BAD: Hardcoded values
const response = await fetch('http://localhost:3000/api/users', {
headers: { 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiJ9...' }
});
// GOOD: Environment variables
const response = await fetch(`${process.env.API_BASE_URL}/api/users`, {
headers: { 'Authorization': `Bearer ${process.env.TEST_TOKEN}` }
});
Mistake 3: Not Validating Response Schemas
Checking that the status code is 200 is not enough. You need to verify the response body structure, data types, and required fields. A 200 response with missing or incorrect data is worse than a 500 because it silently corrupts downstream systems.
// Use JSON Schema validation
const Ajv = require('ajv');
const ajv = new Ajv();
const userSchema = {
type: 'object',
required: ['id', 'name', 'email'],
properties: {
id: { type: 'integer' },
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
createdAt: { type: 'string', format: 'date-time' }
},
additionalProperties: false // Catch unexpected fields
};
test('response matches schema', async () => {
const res = await request(app).get('/api/users/1').expect(200);
const validate = ajv.compile(userSchema);
const valid = validate(res.body);
expect(valid).toBe(true);
if (!valid) console.log(validate.errors);
});
Mistake 4: Ignoring Response Time
An API that returns correct data but takes 10 seconds is broken. Include performance assertions in your tests, and set up monitoring to alert when response times degrade.
Mistake 5: Testing Against Production
Running automated tests against production APIs can create real data, trigger real notifications, and consume real resources. Always use dedicated test environments with isolated databases.
Mistake 6: Not Cleaning Up Test Data
Tests that create data without cleaning up pollute the database and cause other tests to fail unpredictably. Use setup/teardown hooks to ensure each test starts with a known state.
Debugging Tips
- Use verbose curl:
curl -vshows the full request/response including headers, TLS handshake, and redirects. - Check Content-Type: Many "400 Bad Request" errors happen because the Content-Type header is missing or wrong.
- Inspect the raw body: Pretty-printed JSON can hide issues. Check for trailing commas, BOM characters, and encoding problems.
- Compare with a working request: If Postman works but your code does not, compare headers and body byte-for-byte.
- Check CORS: Browser requests fail with opaque errors when CORS is misconfigured. Test with curl first to isolate the issue.
- Read server logs: The client error message is often generic. The server logs contain the actual failure reason.
- Use request IDs: Send an
X-Request-IDheader and search for it in server logs to correlate requests with errors. - Test with our HTTP Tester: Use the DevToolbox HTTP Request Tester to send quick ad-hoc requests and inspect the full response, including headers and timing.
Debug API Responses
When debugging, format messy JSON responses with our JSON Formatter, validate JSON structure with our JSON Validator, and decode JWTs with our JWT Decoder.
13. GraphQL Testing
GraphQL changes the testing paradigm. Instead of multiple endpoints with fixed response shapes, you have a single /graphql endpoint that accepts queries specifying exactly what data to return. This flexibility is powerful but introduces new testing challenges.
Query Testing
# Basic GraphQL query
curl -X POST https://api.example.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"query": "query GetUser($id: ID!) { user(id: $id) { id name email posts { title createdAt } } }",
"variables": { "id": "42" }
}'
# Response
{
"data": {
"user": {
"id": "42",
"name": "Alice",
"email": "alice@example.com",
"posts": [
{ "title": "Hello World", "createdAt": "2026-02-11" }
]
}
}
}
Mutation Testing
# GraphQL mutation
curl -X POST https://api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id name email } }",
"variables": {
"input": {
"name": "Alice",
"email": "alice@example.com",
"password": "SecurePass123!"
}
}
}'
What to Test in GraphQL APIs
- Query depth limiting: Deeply nested queries can DoS your server. Test that queries beyond a certain depth are rejected.
- Query complexity analysis: Complex queries requesting many relations should be limited. Test that your complexity budget is enforced.
- Field-level authorization: Test that users can only query fields they are authorized to see (e.g., admin fields hidden from regular users).
- N+1 query detection: Use DataLoader or similar batching to prevent N+1 database queries. Test with query logging enabled.
- Error handling: GraphQL returns 200 even for errors, with errors in the response body. Test that errors include clear messages without leaking internal details.
- Schema introspection: Decide whether introspection should be enabled in production (security risk) and test accordingly.
- Input validation: Test mutations with invalid input to ensure proper validation errors are returned.
// Jest test for GraphQL
const request = require('supertest');
const app = require('./app');
describe('GraphQL API', () => {
it('fetches user with selected fields', async () => {
const query = `
query {
user(id: "1") {
id
name
email
}
}
`;
const res = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
expect(res.body.data.user).toMatchObject({
id: '1',
name: expect.any(String),
email: expect.any(String)
});
expect(res.body.errors).toBeUndefined();
});
it('rejects deeply nested queries', async () => {
const query = `
query {
user(id: "1") {
posts {
author {
posts {
author {
posts {
title
}
}
}
}
}
}
}
`;
const res = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
expect(res.body.errors).toBeDefined();
expect(res.body.errors[0].message).toContain('depth');
});
});
14. WebSocket Testing Basics
WebSockets provide full-duplex communication between client and server over a single TCP connection. Unlike HTTP's request-response model, WebSockets allow the server to push data to the client at any time. They are used for real-time features like chat, live dashboards, multiplayer games, and collaborative editing.
WebSocket Lifecycle
- Handshake: Client sends an HTTP Upgrade request. Server responds with 101 Switching Protocols.
- Open: Connection established. Both sides can send messages.
- Message: Text or binary frames sent in either direction.
- Close: Either side sends a close frame. Connection terminates.
Testing WebSocket Connections
// JavaScript: Testing WebSocket with ws library
const WebSocket = require('ws');
describe('WebSocket API', () => {
let ws;
beforeEach((done) => {
ws = new WebSocket('ws://localhost:8080/ws');
ws.on('open', done);
});
afterEach(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
});
it('receives welcome message on connect', (done) => {
ws.on('message', (data) => {
const message = JSON.parse(data);
expect(message.type).toBe('welcome');
expect(message.connectionId).toBeDefined();
done();
});
});
it('echoes messages back', (done) => {
ws.send(JSON.stringify({ type: 'echo', content: 'Hello' }));
ws.on('message', (data) => {
const message = JSON.parse(data);
if (message.type === 'echo_response') {
expect(message.content).toBe('Hello');
done();
}
});
});
it('handles invalid JSON gracefully', (done) => {
ws.send('not valid json');
ws.on('message', (data) => {
const message = JSON.parse(data);
expect(message.type).toBe('error');
expect(message.message).toContain('Invalid JSON');
done();
});
});
it('disconnects after authentication timeout', (done) => {
// Don't send auth message
ws.on('close', (code, reason) => {
expect(code).toBe(4001);
expect(reason.toString()).toContain('Authentication timeout');
done();
});
});
});
Python: websockets Library
import asyncio
import json
import pytest
import websockets
@pytest.mark.asyncio
async def test_websocket_connection():
async with websockets.connect("ws://localhost:8080/ws") as ws:
# Receive welcome message
message = json.loads(await ws.recv())
assert message["type"] == "welcome"
# Send a message
await ws.send(json.dumps({"type": "ping"}))
# Receive response
response = json.loads(await ws.recv())
assert response["type"] == "pong"
@pytest.mark.asyncio
async def test_websocket_broadcast():
async with websockets.connect("ws://localhost:8080/ws") as ws1, \
websockets.connect("ws://localhost:8080/ws") as ws2:
# Skip welcome messages
await ws1.recv()
await ws2.recv()
# Client 1 sends a broadcast message
await ws1.send(json.dumps({
"type": "broadcast",
"content": "Hello everyone"
}))
# Client 2 should receive it
message = json.loads(await ws2.recv())
assert message["type"] == "broadcast"
assert message["content"] == "Hello everyone"
What to Test in WebSocket APIs
- Connection establishment: Verify the WebSocket handshake succeeds and the connection opens.
- Authentication: Test that unauthenticated connections are rejected or time out.
- Message serialization: Verify JSON messages are parsed correctly and invalid messages return errors.
- Reconnection: Test that clients reconnect gracefully after disconnections.
- Heartbeat/ping-pong: Verify keep-alive mechanisms work and stale connections are cleaned up.
- Concurrent connections: Test behavior with many simultaneous connections.
- Backpressure: Test what happens when the server sends data faster than the client can consume it.
- Graceful shutdown: Test that the server closes all connections cleanly during shutdown.
15. Frequently Asked Questions
What is API testing and why is it important?
API testing is the process of verifying that an application programming interface (API) works correctly, reliably, and securely. It is important because APIs are the backbone of modern software architecture, connecting frontends to backends, microservices to each other, and third-party integrations. Without thorough API testing, bugs in one service can cascade across an entire system.
What are the main types of API tests?
The main types of API tests are: unit tests (testing individual endpoints in isolation), integration tests (testing how multiple APIs work together), contract tests (verifying API schemas remain compatible), load/performance tests (measuring throughput and response times under stress), and security tests (checking for vulnerabilities like injection, broken authentication, and data exposure).
What is the difference between PUT and PATCH HTTP methods?
PUT replaces the entire resource with the data you send, meaning you must include all fields even if only one changed. PATCH applies a partial update, allowing you to send only the fields that need to change. PUT is idempotent (same request always produces same result), and PATCH can also be idempotent depending on implementation. Use PUT when replacing an entire document and PATCH for small field-level updates.
What tools can I use for API testing?
Popular API testing tools include curl (command-line), Postman (GUI with collections and environments), Insomnia (lightweight REST client), Thunder Client (VS Code extension), and browser-based tools like the DevToolbox HTTP Tester. For automated testing, frameworks like Jest with supertest (JavaScript), pytest with requests (Python), and net/http/httptest (Go) are widely used.
How do I authenticate API requests?
Common API authentication methods include Bearer tokens (sent in the Authorization header), API keys (sent as a header or query parameter), Basic Auth (base64-encoded username:password), and OAuth 2.0 (token-based flows for delegated authorization). The right method depends on your use case: API keys for server-to-server, OAuth for user-facing apps, and Bearer tokens for stateless APIs.
What is the difference between REST and GraphQL for API testing?
REST uses multiple endpoints with fixed data structures (one URL per resource), while GraphQL uses a single endpoint where clients specify exactly what data they need via queries. Testing REST involves verifying each endpoint with different HTTP methods, while testing GraphQL requires validating queries, mutations, variables, and schema introspection. GraphQL can reduce over-fetching but adds complexity in query validation and depth limiting.
How do I handle rate limiting in API tests?
To handle rate limiting in API tests: check response headers (X-RateLimit-Remaining, Retry-After), implement exponential backoff for retries, use separate test API keys with higher limits, mock rate-limited responses in unit tests, and add delays between requests in integration tests. Always test that your application gracefully handles 429 Too Many Requests responses.
What are common API testing mistakes to avoid?
Common API testing mistakes include: only testing happy paths (ignoring error cases), hardcoding test data instead of using environment variables, not validating response schemas, ignoring response times and performance, testing against production APIs, not cleaning up test data, skipping authentication and authorization tests, and not testing edge cases like empty arrays, null values, and special characters.
Conclusion
API testing is not a single activity but a layered discipline. Unit tests catch logic errors in individual handlers. Integration tests verify that your services work together with real databases and middleware. Contract tests protect consumers from breaking changes. Load tests reveal performance bottlenecks before your users do. Security tests guard against the OWASP API Top 10.
The key to success is building API testing into your development workflow from day one. Write tests as you build endpoints. Run them in CI on every commit. Monitor response times and error rates in production. Document your APIs with OpenAPI specs and keep them synchronized with the implementation.
Whether you are debugging a flaky 502 with curl, validating a JWT with our decoder, or load-testing a new endpoint with k6, the techniques in this guide give you the foundation to test APIs at every level of the stack. Start with the basics -- send a request, check the response -- and build from there.
Start Testing APIs Now
Put this guide into practice with our HTTP Request Tester. Send requests to any API, set custom headers and authentication, and inspect the full response -- all in your browser with zero setup. Pair it with our JSON Formatter, JSON Validator, JWT Decoder, and URL Encoder for a complete API debugging toolkit.