Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Modern web applications are built on APIs. If you only test through the UI, you're testing one layer of a multi-layer system — and you'll never isolate whether a bug is in the frontend, the API, or the database. Testing APIs directly — sending HTTP requests with specific headers, authentication tokens, and bodies, then asserting on the status code, response schema, and values — lets you test the contract independently of any UI. It also exposes cases the UI never sends: invalid inputs, missing fields, oversized payloads, wrong auth tokens. The jump from 'I can click through the UI' to 'I can test the API directly' is the jump from junior QA to mid-level QA.
Requests-based API tests expose the contract between client and server independently of any UI — the same assertion patterns (status code, required fields, type checks) translate directly to Postman test scripts, pytest fixtures, or a proper test framework. Testing APIs directly reveals the edge cases no UI ever sends: missing fields, oversized payloads, wrong auth schemes, and boundary values that the frontend happily validates away before the API ever sees them.
import requests
BASE = "https://jsonplaceholder.typicode.com"
def test_get_user_returns_correct_schema():
resp = requests.get(f"{BASE}/users/1")
assert resp.status_code == 200, f"Expected 200, got {resp.status_code}"
user = resp.json()
required_fields = ["id", "name", "email", "address", "phone", "website", "company"]
for field in required_fields:
assert field in user, f"Missing field: {field}"
assert isinstance(user["id"], int)
assert "@" in user["email"], "email field is not a valid email"
print(f"✓ GET /users/1 name={user['name']} email={user['email']}")
def test_create_post_returns_201():
payload = {"title": "QA Test Post", "body": "Testing API contract", "userId": 1}
resp = requests.post(f"{BASE}/posts", json=payload)
assert resp.status_code == 201, f"Expected 201 Created, got {resp.status_code}"
created = resp.json()
assert created["title"] == payload["title"], "Response title doesn't match request"
assert "id" in created, "Response missing 'id' field"
print(f"✓ POST /posts id={created['id']} title={created['title']}")
def test_missing_required_field_returns_400():
# Send a post with no userId — API should reject it
resp = requests.post(f"{BASE}/posts", json={"title": "no userId"})
# jsonplaceholder is lenient (returns 201) — in a real API this should be 400
print(f" POST /posts (missing userId) → {resp.status_code} (real API should be 400)")
def test_nonexistent_resource_returns_404():
resp = requests.get(f"{BASE}/users/9999")
assert resp.status_code == 404, f"Expected 404, got {resp.status_code}"
print(f"✓ GET /users/9999 → 404 (correct)")
for test in [test_get_user_returns_correct_schema, test_create_post_returns_201,
test_missing_required_field_returns_400, test_nonexistent_resource_returns_404]:
try:
test()
except AssertionError as e:
print(f"✗ {test.__name__}: {e}")python3 main.pyPATCH /posts/1 — send {title: 'Updated title'} and assert the response contains the updated title. Then test that PATCH /posts/9999 returns 404 (non-existent resource). This covers the full CRUD lifecycle.body field to a string of 100,000 characters. Does the API return 413 (Payload Too Large) or accept it? For a real API, this is an important edge case test.GET /users/1 with a Authorization: Bearer invalid-token header. Does the API reject it with 401? (jsonplaceholder won't, but your test should document what a real API should do.)Use these three in order. Each builds on the one before.
In one paragraph, explain HTTP status codes that a QA engineer must know by heart: 200, 201, 400, 401, 403, 404, 409, 422, 500. Give a concrete scenario for each.
Walk me through the difference between testing a REST API and testing the UI that calls it. What bugs can you find by testing the API that the UI testing would miss, and vice versa?
I'm testing an API that has 50 endpoints. I have 3 days. Walk me through how to risk-prioritize: which endpoints to test first, which HTTP methods and status codes to cover for each, what negative test cases (invalid input, missing auth, oversized payload) to always include, and how to decide when coverage is 'good enough' given the time constraint.