Testing APIs with Postman
Postman is a handy tool for exploring and debugging APIs.
What to test
Section titled “What to test”- URL + method
- Query parameters
- Headers (Content-Type, Authorization)
- JSON body
- Status codes
- Response schema
Common Postman setup
Section titled “Common Postman setup”POST JSON
Section titled “POST JSON”- Method: POST
- Body: raw → JSON
Example payload:
{ "title": "Buy milk" }Add headers
Section titled “Add headers”Content-Type: application/json
Auth testing
Section titled “Auth testing”When you add token auth later, you’ll often set:
Authorization: Bearer <token>
Postman can store variables and environments for tokens.
Debugging tips
Section titled “Debugging tips”- If Flask returns 415 Unsupported Media Type, your Content-Type might be wrong.
- If you get 400, inspect validation errors.
- If you get 401/403, inspect Authorization header.
Bonus: keep docs in sync
Section titled “Bonus: keep docs in sync”If you maintain an API, consider documenting endpoints with:
- OpenAPI (Swagger)
That’s a more advanced step, but worth it for real projects.
What an API test is actually checking
Section titled “What an API test is actually checking”Postman is a graphical client for constructing HTTP requests. Everything it sends is ordinary HTTP, so the things worth asserting are the same whether you click them or script them:
flowchart TD R["a request"] --> M["method and URL"] R --> H["headers: Content-Type, Authorization, Accept"] R --> B["body: JSON, form, or raw"] M --> S["assert the STATUS code"] H --> C["assert the response Content-Type"] B --> J["assert the body SHAPE, not just its text"]
Content-Type decides how your body is read
Section titled “Content-Type decides how your body is read”Measured, sending {"name": "ada"} three ways to the same endpoint:
| how it was sent | Content-Type | request.is_json | request.json | request.form |
|---|---|---|---|---|
| JSON body | application/json | True | {'name': 'ada'} | {} |
| form fields | application/x-www-form-urlencoded | False | None | {'name': 'ada'} |
| raw string, no header | None | False | None | {} |
The third row is the one that wastes afternoons: the body was sent, the server received
15 bytes of it, and request.json is None purely because no Content-Type was set. In
Postman this is the difference between the raw + JSON body type and raw + Text.
curl -X POST http://localhost:5000/echo \
-H "Content-Type: application/json" \
-d '{"name": "ada"}'The error codes you should be provoking
Section titled “The error codes you should be provoking”Measured against a real endpoint:
| what you send | status |
|---|---|
| valid JSON with the right header | 200 |
malformed JSON with application/json | 400 |
valid JSON with Content-Type: text/plain | 415 |
415 Unsupported Media Type is a genuinely useful signal — the body was fine and the
header was wrong — and it is easy to misread as a server bug.
request.get_json() # raises -> 400 on a malformed body
request.get_json(silent=True) # returns None instead, measured 200A checklist worth running against every endpoint
Section titled “A checklist worth running against every endpoint”| case | expect |
|---|---|
| the happy path | 200 or 201, correct body shape |
| a required field missing | 400 or 422, an error naming the field |
| no credentials | 401 |
| valid credentials, someone else’s record | 403, not 404 and not 200 |
| an id that does not exist | 404 |
| a duplicate of something unique | 409 |
| the same request repeated quickly | 429 once limited |
The fourth row is the one most often missed. Fetching /orders/999 while logged in as a
different user must not return that order — an ownership check is not the same as an
authentication check, and only a deliberate test catches it.
client.post("/echo", json={}, headers={"Authorization": "Bearer abc123"})
# the app sees: 'Bearer abc123'Automate what you have proved by hand
Section titled “Automate what you have proved by hand”Postman is excellent for exploring an API and poor as the only place your tests live — a collection on one laptop is not something CI can run. Once a case is understood, move it into the test suite:
def test_create_requires_name(client):
r = client.post("/items", json={})
assert r.status_code == 400
assert "name" in r.get_json()["message"]
def test_cannot_read_another_users_order(client, other_users_token):
r = client.get("/orders/999", headers={"Authorization": other_users_token})
assert r.status_code == 403See it move
Section titled “See it move”Check yourself
Section titled “Check yourself”-
You send a correct JSON body but forget the Content-Type header. What does request.json contain?
Measured: the server received 15 bytes and request.json was still None. In Postman this is the difference between the raw JSON body type and raw Text.
pch.quizShowAnswer
B — None, because Flask parses the body according to the header — Measured: the server received 15 bytes and request.json was still None. In Postman this is the difference between the raw JSON body type and raw Text.
-
Valid JSON is sent with Content-Type: text/plain. What status does Flask return?
415 Unsupported Media Type: the body was fine and the header was wrong. A malformed body with the correct header measured 400 instead.
pch.quizShowAnswer
C — 415 — 415 Unsupported Media Type: the body was fine and the header was wrong. A malformed body with the correct header measured 400 instead.
-
Which test case is most often missed when testing an API by hand?
Authentication and ownership are different checks. Logging in proves who you are, not that a given record is yours, and only a deliberate test catches the gap.
pch.quizShowAnswer
C — an authenticated user requesting someone else's record must get 403 — Authentication and ownership are different checks. Logging in proves who you are, not that a given record is yours, and only a deliberate test catches the gap.
-
Why should a Postman collection not be where your API tests live permanently?
Postman is excellent for exploring an API. Once a case is understood it belongs in code that runs on every commit.
pch.quizShowAnswer
B — a collection on one machine is not something CI can run, so move proven cases into the test suite — Postman is excellent for exploring an API. Once a case is understood it belongs in code that runs on every commit.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading