Why this matters on the job
Open any QA job post on Naukri or LinkedIn for a 2–6 year role and you will see the same line: "Hands-on experience in API testing using Postman / REST Assured." The reason is simple. A modern web app, its Android app and its iOS app all talk to the same backend APIs. If the /orders API returns the wrong total, every channel is broken — and you can catch that bug in milliseconds at the API layer instead of waiting for a slow, flaky UI test.
In real projects, testers who can read a raw HTTP request, reproduce a bug with a single curl command and paste it into the Jira ticket are taken far more seriously by developers. "Login is not working" gets ignored; "POST /auth returns 200 with {"reason":"Bad credentials"} instead of 401 — curl attached" gets fixed. This lesson gives you that vocabulary and that skill.
Concepts
Anatomy of an HTTP request and response
Every API call is a request from a client and a response from a server. A request has a method (GET, POST…), a URL (scheme + host + path + optional query string), headers (metadata such as Content-Type) and an optional body. A response has a status line (e.g. HTTP/1.1 200 OK), headers, and usually a body (JSON in most REST APIs).
POST /booking HTTP/1.1 <- method + path + protocol
Host: restful-booker.herokuapp.com <- headers start
Content-Type: application/json
Accept: application/json
{"firstname":"Aarav","lastname":"Sharma"} <- body
HTTP/1.1 200 OK <- status line
Content-Type: application/json; charset=utf-8
{"bookingid":1234,"booking":{...}} <- response bodyREST (Representational State Transfer) is an architectural style, not a protocol. Its key ideas: everything is a resource with a URL (/booking/12), you act on resources with standard HTTP methods, every request is stateless (the server keeps no session between calls — auth travels with each request), and responses are representations such as JSON.
HTTP methods, safety and idempotency
A method is safe if it does not change server state. It is idempotent if sending the same request once or ten times leaves the server in the same state. Testers care because retries happen constantly — mobile networks drop, load balancers retry, users double-click "Pay".
| Method | Purpose | Safe | Idempotent | Typical success code |
|---|---|---|---|---|
| GET | Read a resource or list | Yes | Yes | 200 OK |
| POST | Create a resource / trigger an action | No | No | 201 Created (+ Location header) |
| PUT | Replace the whole resource | No | Yes | 200 OK or 204 No Content |
| PATCH | Update some fields | No | Not guaranteed | 200 OK |
| DELETE | Remove a resource | No | Yes | 204 No Content or 200 OK |
| HEAD / OPTIONS | Headers only / allowed methods (CORS preflight) | Yes | Yes | 200 / 204 |
Payment gateways solve the "POST is not idempotent" problem with an Idempotency-Key header: the client sends a unique key, and if the same key arrives twice the server returns the first result instead of charging the customer twice. "How would you test a double-click on Pay?" is a favourite product-company interview question — the answer starts here.
Status codes every tester must know
| Code | Meaning | When you typically see it |
|---|---|---|
| 200 OK | Success with body | GET, PUT, PATCH |
| 201 Created | Resource created | POST (should include Location) |
| 204 No Content | Success, empty body | DELETE, some PUTs |
| 301 / 302 / 304 | Moved / Found / Not Modified | Redirects, caching with ETag |
| 400 Bad Request | Malformed or invalid input | Missing field, wrong type |
| 401 Unauthorized | Not authenticated (who are you?) | Missing/expired token |
| 403 Forbidden | Authenticated but not allowed | User A editing User B's data |
| 404 Not Found | Resource doesn't exist | Wrong id or path |
| 405 Method Not Allowed | Method not supported on this path | DELETE on a read-only endpoint |
| 409 Conflict | State conflict | Duplicate email on sign-up |
| 415 Unsupported Media Type | Wrong Content-Type | Sending XML to a JSON API |
| 422 Unprocessable Entity | Valid JSON, failed business rules | Checkout date before check-in |
| 429 Too Many Requests | Rate limited | OTP API hammered |
| 500 / 502 / 503 / 504 | Server error / bad gateway / unavailable / timeout | Always a bug or infra issue to report |
Headers you will use daily
| Header | Direction | Why testers care |
|---|---|---|
Content-Type | Request & response | Format of the body being sent/returned |
Accept | Request | Format the client wants back |
Authorization | Request | Basic / Bearer credentials |
Cookie / Set-Cookie | Both | Session or token cookies (restful-booker uses Cookie: token=…) |
Cache-Control, ETag | Response | Stale data bugs, 304 behaviour |
Location | Response | URL of a newly created resource |
X-Request-Id / correlation id | Both | Paste in bug reports so devs can find logs |
Hands-on Lab: curl lab on Restful Booker
Target: Restful Booker (public practice API — data resets periodically, other learners use it too, so always create your own data). Docs: restful-booker API docs. Keep a notes table with columns Request, Expected, Actual, Observation.
- Verify tools. On Windows use Git Bash (CMD/PowerShell quoting differs). Install jq:
brew install jq(macOS),sudo apt install jq(Ubuntu) orwinget install jqlang.jq(Windows).curl --version jq --version - Health check — look at the status line and headers (
-iincludes headers). Expected:HTTP/1.1 201 Created. Note it: a health check returning 201 is a quirk, most APIs return 200.curl -i https://restful-booker.herokuapp.com/ping - List bookings and show only the first three with jq. Then filter with a query parameter.
curl -s https://restful-booker.herokuapp.com/booking | jq '.[0:3]' curl -s "https://restful-booker.herokuapp.com/booking?firstname=Aarav" | jq - Create a booking (POST) and capture the id into a shell variable. Expected: JSON with
bookingidand the echoedbooking. Check the status code: it is 200, while REST convention says 201 — log it as an observation.BASE=https://restful-booker.herokuapp.com BODY='{"firstname":"Aarav","lastname":"Sharma","totalprice":1500,"depositpaid":true,"bookingdates":{"checkin":"2026-12-01","checkout":"2026-12-05"},"additionalneeds":"Breakfast"}' curl -s -i -X POST "$BASE/booking" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d "$BODY" BOOKING_ID=$(curl -s -X POST "$BASE/booking" \ -H "Content-Type: application/json" -H "Accept: application/json" \ -d "$BODY" | jq -r '.bookingid') echo "Created booking: $BOOKING_ID" - Prove POST is not idempotent: run the second command above twice and confirm you get two different ids.
- Get an auth token (credentials are public on the restful-booker docs page).
TOKEN=$(curl -s -X POST "$BASE/auth" \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"password123"}' | jq -r '.token') echo "$TOKEN" - Prove PUT is idempotent: send the same full update twice. Both responses must be identical and a GET afterwards must show the same state.
UPDATE='{"firstname":"Aarav","lastname":"Sharma","totalprice":2000,"depositpaid":false,"bookingdates":{"checkin":"2026-12-02","checkout":"2026-12-06"},"additionalneeds":"Lunch"}' for i in 1 2; do curl -s -X PUT "$BASE/booking/$BOOKING_ID" \ -H "Content-Type: application/json" -H "Accept: application/json" \ -H "Cookie: token=$TOKEN" -d "$UPDATE"; echo done curl -s -H "Accept: application/json" "$BASE/booking/$BOOKING_ID" | jq - PATCH only one field and confirm the rest is unchanged.
curl -s -X PATCH "$BASE/booking/$BOOKING_ID" \ -H "Content-Type: application/json" -H "Accept: application/json" \ -H "Cookie: token=$TOKEN" -d '{"additionalneeds":"Airport pickup"}' | jq - Negative checks. Expected: PUT without token →
403; unknown id →404.-wprints just the status and time.curl -s -o /dev/null -w "no-token PUT => %{http_code}\n" -X PUT "$BASE/booking/$BOOKING_ID" \ -H "Content-Type: application/json" -d "$UPDATE" curl -s -o /dev/null -w "unknown id => %{http_code} in %{time_total}s\n" \ -H "Accept: application/json" "$BASE/booking/999999999" - DELETE and verify. Expected: DELETE returns
201 Created(another quirk — 204/200 is conventional), then GET returns404.curl -s -o /dev/null -w "delete => %{http_code}\n" -X DELETE "$BASE/booking/$BOOKING_ID" -H "Cookie: token=$TOKEN" curl -s -o /dev/null -w "get after delete => %{http_code}\n" "$BASE/booking/$BOOKING_ID" - Run one call with
-vand identify: the TLS handshake, the request headers curl sent (lines starting with>) and the response headers (<). Note theX-Powered-Byheader — we will revisit it in the security lesson.
By the end you should have recorded at least three convention deviations: /ping returns 201, POST /booking returns 200 instead of 201, and DELETE returns 201. In a real project each of these becomes a conversation with the developer or a documented "known behaviour" — never something you silently accept.
Common mistakes
- Forgetting
Content-Type: application/jsonon POST/PUT — the server cannot parse the body and you report a false bug. - Copying Linux curl commands into Windows CMD; single quotes don't work there. Use Git Bash or Postman.
- Checking only the status code. A 200 with an error message in the body (restful-booker's bad-credentials response) is still a failure.
- Mixing up 401 and 403: 401 = not logged in, 403 = logged in but not permitted.
- Using shared/production data. Always create the record you test, then clean it up.
Real-world assignment
Your lead shares the JSONPlaceholder API and asks for a "curl smoke pack" before the sprint demo. Deliver a smoke.sh script with 10 curl checks covering GET /posts, GET /posts/1, GET /posts/1/comments, GET /comments?postId=1, POST /posts (expect 201 and id: 101), PUT, PATCH, DELETE /posts/1, GET /posts/0 (expect 404) and GET /users/1. Each line must print name, expected code, actual code, PASS/FAIL. Add one sample bug report in the format Title / Environment / curl / Expected / Actual / Evidence.
Key takeaways
- Every API call = method + URL + headers + body; every response = status + headers + body.
- GET, PUT and DELETE are idempotent; POST is not — that is why retries and double-clicks are a testing priority.
- Assert status code and body and important headers.
- curl + jq is the fastest way to reproduce and share an API bug.
- Document convention deviations (200 vs 201, 201 on DELETE) instead of ignoring them.