TL;DR
- API testing verifies backend services work correctly without UI — faster and more reliable than E2E tests
- Test: status codes, response body, headers, error handling, authentication, performance
- Tools: Postman (manual/learning), REST Assured (Java), Supertest (Node.js), requests (Python)
- Automate in CI/CD — APIs change frequently, catch breaking changes early
- Cover both happy path and error scenarios (400s, 401, 404, 500)
Best for: Backend developers, QA engineers, anyone testing microservices Skip if: You only need to test static websites or simple frontends Read time: 18 minutes
Your frontend tests pass. Users report the app is broken. The API changed, and nobody tested the contract.
API testing catches these issues before they reach users. It’s faster than UI testing, more reliable, and tests the actual business logic your application depends on.
This tutorial teaches API testing from scratch — HTTP basics, REST conventions, authentication, error handling, and automation with popular tools.
What is API Testing?
API (Application Programming Interface) testing verifies that your backend services work correctly. Instead of clicking through a UI, you send HTTP requests directly to endpoints and verify responses.
What API testing covers:
- Functionality — does the endpoint do what it should?
- Data validation — are responses structured correctly?
- Error handling — does it fail gracefully?
- Authentication — is access properly controlled?
- Performance — can it handle load?
Why API testing matters:
- Faster than UI tests — no browser rendering, milliseconds vs seconds
- More stable — no flaky selectors or timing issues
- Earlier feedback — test before frontend exists
- Better coverage — test edge cases impossible via UI
HTTP Fundamentals
Before testing APIs, understand HTTP basics.
HTTP Methods
GET /users # Retrieve all users
GET /users/123 # Retrieve user 123
POST /users # Create new user
PUT /users/123 # Replace user 123
PATCH /users/123 # Update parts of user 123
DELETE /users/123 # Delete user 123
| Method | Purpose | Has Body | Idempotent |
|---|---|---|---|
| GET | Read data | No | Yes |
| POST | Create resource | Yes | No |
| PUT | Replace resource | Yes | Yes |
| PATCH | Partial update | Yes | No |
| DELETE | Remove resource | Optional | Yes |
Status Codes
2xx Success
├── 200 OK # Request succeeded
├── 201 Created # Resource created
├── 204 No Content # Success, nothing to return
4xx Client Errors
├── 400 Bad Request # Invalid input
├── 401 Unauthorized # Missing/invalid auth
├── 403 Forbidden # Valid auth, no permission
├── 404 Not Found # Resource doesn't exist
├── 409 Conflict # Conflicts with current state
├── 422 Unprocessable # Validation failed
5xx Server Errors
├── 500 Internal Error # Server bug
├── 502 Bad Gateway # Upstream error
├── 503 Unavailable # Server overloaded/maintenance
Request Structure
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
{
"name": "John Doe",
"email": "john@example.com"
}
Response Structure
HTTP/1.1 201 Created
Content-Type: application/json
X-Request-Id: abc123
{
"id": 456,
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2026-01-20T10:30:00Z"
}
Testing with Postman
Postman is the easiest way to start API testing.
First Request
- Open Postman
- Enter URL:
https://jsonplaceholder.typicode.com/posts/1 - Method: GET
- Click Send
Response:
{
"userId": 1,
"id": 1,
"title": "sunt aut facere...",
"body": "quia et suscipit..."
}
Adding Tests
In Postman, add JavaScript tests in the “Tests” tab:
// Status code check
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
// Response time
pm.test("Response time is less than 500ms", function () {
pm.expect(pm.response.responseTime).to.be.below(500);
});
// Body validation
pm.test("Has correct structure", function () {
const json = pm.response.json();
pm.expect(json).to.have.property("id");
pm.expect(json).to.have.property("title");
pm.expect(json.id).to.eql(1);
});
// Header check
pm.test("Content-Type is JSON", function () {
pm.expect(pm.response.headers.get("Content-Type"))
.to.include("application/json");
});
Collections and Variables
Organize tests into collections:
// Environment variables
pm.environment.set("baseUrl", "https://api.example.com");
pm.environment.set("token", responseJson.token);
// Use variables in URL
// {{baseUrl}}/users/{{userId}}
// Use in headers
// Authorization: Bearer {{token}}
REST API Testing with Code
Python with requests
import requests
import pytest
BASE_URL = "https://api.example.com"
class TestUsersAPI:
def test_get_users_returns_list(self):
response = requests.get(f"{BASE_URL}/users")
assert response.status_code == 200
assert isinstance(response.json(), list)
def test_create_user(self):
payload = {
"name": "John Doe",
"email": "john@example.com"
}
response = requests.post(
f"{BASE_URL}/users",
json=payload,
headers={"Content-Type": "application/json"}
)
assert response.status_code == 201
data = response.json()
assert data["name"] == payload["name"]
assert "id" in data
def test_get_nonexistent_user(self):
response = requests.get(f"{BASE_URL}/users/99999")
assert response.status_code == 404
def test_create_user_invalid_email(self):
payload = {"name": "John", "email": "not-an-email"}
response = requests.post(f"{BASE_URL}/users", json=payload)
assert response.status_code == 400
assert "email" in response.json().get("error", "").lower()
JavaScript with Supertest
const request = require('supertest');
const app = require('../src/app');
describe('Users API', () => {
test('GET /users returns list of users', async () => {
const response = await request(app)
.get('/users')
.expect(200)
.expect('Content-Type', /json/);
expect(Array.isArray(response.body)).toBe(true);
});
test('POST /users creates new user', async () => {
const newUser = {
name: 'John Doe',
email: 'john@example.com'
};
const response = await request(app)
.post('/users')
.send(newUser)
.expect(201);
expect(response.body).toMatchObject(newUser);
expect(response.body.id).toBeDefined();
});
test('GET /users/:id returns 404 for unknown user', async () => {
await request(app)
.get('/users/99999')
.expect(404);
});
});
Java with REST Assured
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.*;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
public class UsersApiTest {
@BeforeAll
public static void setup() {
RestAssured.baseURI = "https://api.example.com";
}
@Test
public void getUsersReturnsList() {
given()
.when()
.get("/users")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("$", instanceOf(List.class));
}
@Test
public void createUserReturnsCreated() {
String requestBody = """
{
"name": "John Doe",
"email": "john@example.com"
}
""";
given()
.contentType(ContentType.JSON)
.body(requestBody)
.when()
.post("/users")
.then()
.statusCode(201)
.body("name", equalTo("John Doe"))
.body("id", notNullValue());
}
}
Authentication Testing
Basic Authentication
import requests
from requests.auth import HTTPBasicAuth
response = requests.get(
"https://api.example.com/protected",
auth=HTTPBasicAuth("username", "password")
)
Bearer Token (JWT)
# Step 1: Login to get token
login_response = requests.post(
"https://api.example.com/auth/login",
json={"email": "user@example.com", "password": "secret"}
)
token = login_response.json()["token"]
# Step 2: Use token in requests
response = requests.get(
"https://api.example.com/protected",
headers={"Authorization": f"Bearer {token}"}
)
API Key
# In header
response = requests.get(
"https://api.example.com/data",
headers={"X-API-Key": "your-api-key"}
)
# In query parameter
response = requests.get(
"https://api.example.com/data?api_key=your-api-key"
)
Testing Auth Scenarios
class TestAuthentication:
def test_protected_endpoint_requires_auth(self):
response = requests.get(f"{BASE_URL}/protected")
assert response.status_code == 401
def test_invalid_token_rejected(self):
response = requests.get(
f"{BASE_URL}/protected",
headers={"Authorization": "Bearer invalid_token"}
)
assert response.status_code == 401
def test_expired_token_rejected(self):
expired_token = create_expired_token()
response = requests.get(
f"{BASE_URL}/protected",
headers={"Authorization": f"Bearer {expired_token}"}
)
assert response.status_code == 401
def test_valid_token_grants_access(self):
token = get_valid_token()
response = requests.get(
f"{BASE_URL}/protected",
headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == 200
GraphQL Testing
GraphQL uses a single endpoint with queries and mutations.
Query Testing
def test_graphql_query():
query = """
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
"""
response = requests.post(
f"{BASE_URL}/graphql",
json={
"query": query,
"variables": {"id": "123"}
}
)
assert response.status_code == 200
data = response.json()
assert "errors" not in data
assert data["data"]["user"]["id"] == "123"
Mutation Testing
def test_graphql_mutation():
mutation = """
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
"""
response = requests.post(
f"{BASE_URL}/graphql",
json={
"query": mutation,
"variables": {
"input": {
"name": "John Doe",
"email": "john@example.com"
}
}
}
)
assert response.status_code == 200
data = response.json()
assert data["data"]["createUser"]["name"] == "John Doe"
Error Testing
Test how your API handles problems.
class TestErrorHandling:
def test_malformed_json_returns_400(self):
response = requests.post(
f"{BASE_URL}/users",
data="not valid json",
headers={"Content-Type": "application/json"}
)
assert response.status_code == 400
def test_missing_required_field_returns_400(self):
response = requests.post(
f"{BASE_URL}/users",
json={"name": "John"} # missing email
)
assert response.status_code == 400
assert "email" in response.json()["message"].lower()
def test_duplicate_email_returns_409(self):
# Create first user
requests.post(f"{BASE_URL}/users", json={
"name": "John", "email": "john@example.com"
})
# Try creating duplicate
response = requests.post(f"{BASE_URL}/users", json={
"name": "Jane", "email": "john@example.com"
})
assert response.status_code == 409
def test_error_response_format(self):
response = requests.get(f"{BASE_URL}/users/nonexistent")
assert response.status_code == 404
error = response.json()
assert "error" in error or "message" in error
Performance Testing
Test API response times and throughput.
Basic Performance Check
import time
def test_response_time():
start = time.time()
response = requests.get(f"{BASE_URL}/users")
duration = time.time() - start
assert response.status_code == 200
assert duration < 0.5 # Under 500ms
Load Testing with k6
// k6 script: load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 20 }, // Ramp to 20 users
{ duration: '1m', target: 20 }, // Stay at 20 users
{ duration: '10s', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% under 500ms
http_req_failed: ['rate<0.01'], // Less than 1% failures
},
};
export default function () {
const response = http.get('https://api.example.com/users');
check(response, {
'status is 200': (r) => r.status === 200,
'response time OK': (r) => r.timings.duration < 500,
});
sleep(1);
}
k6 run load-test.js
CI/CD Integration
GitHub Actions
# .github/workflows/api-tests.yml
name: API Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: pip install pytest requests
- name: Run API tests
run: pytest tests/api/ -v
- name: Run Postman collection
run: |
npm install -g newman
newman run collection.json -e environment.json
API Testing Checklist
For Every Endpoint
- Happy path returns correct status and data
- Invalid input returns 400 with helpful message
- Missing auth returns 401
- Insufficient permissions return 403
- Not found returns 404
- Response time under threshold
- Response headers are correct
- Response body matches schema
Security Tests
- SQL injection payloads rejected
- XSS payloads escaped in responses
- Rate limiting works
- Sensitive data not leaked in errors
- CORS properly configured
AI-Assisted API Testing
AI tools can accelerate API test development.
What AI does well:
- Generate test cases from OpenAPI/Swagger specs
- Create valid and invalid test data
- Write boilerplate for common patterns
- Suggest edge cases to test
What still needs humans:
- Understanding business requirements
- Designing test strategy
- Debugging flaky tests
- Interpreting performance results
Useful prompt:
I have this REST API endpoint:
POST /api/orders
Body: { customerId: string, items: [{productId: string, quantity: number}], couponCode?: string }
Returns: { orderId: string, total: number, status: string }
Generate test cases covering:
- Valid order with multiple items
- Empty items array
- Invalid customerId
- Negative quantity
- Invalid coupon code
- Valid coupon application
FAQ
What is API testing?
API testing verifies that APIs work correctly by sending HTTP requests and validating responses. It tests functionality, data validation, error handling, authentication, and performance. Unlike UI testing, API testing directly tests the business logic layer, making it faster and more reliable.
What tools are used for API testing?
Popular tools include:
- Postman — GUI tool for manual testing and automation
- REST Assured — Java library for API testing
- Supertest — Node.js/JavaScript API testing
- requests + pytest — Python API testing
- k6 — Performance and load testing
- Newman — CLI runner for Postman collections
What is the difference between API testing and unit testing?
Unit tests verify individual functions in isolation, mocking all dependencies. API tests verify complete HTTP endpoints, including routing, middleware, authentication, database operations, and response formatting. API tests are integration tests that verify components work together. Both are needed for comprehensive coverage.
How do I test authenticated APIs?
- Send login request with credentials
- Extract token from response
- Include token in Authorization header for subsequent requests
- Store token in environment variable for reuse
- Implement token refresh for expiring tokens
- Test both authenticated and unauthenticated scenarios
Official Resources
See Also
- Postman Tutorial - Complete guide to API testing with Postman
- REST Assured Guide - Java API testing framework
- GraphQL Testing Guide - Testing GraphQL APIs
- API Performance Testing - Load and stress testing APIs
- Contract Testing with Pact - Consumer-driven contract testing
