Why PUT and PATCH Are Not Interchangeable: Mapping CRUD onto HTTP
A tester on your team sends POST /orders twice with the same body and gets 201 Created twice, then sends PUT /orders/42 carrying only {"notes": "rush"} and discovers the order lost its customer id. Neither request is wrong — the tester is. Before you write a single assertion you need an exact map from CRUD operations to HTTP methods, and from each method to the status codes a compliant server may return, because most "API bugs" filed in a tester's first month are contract misunderstandings rather than defects.
The CRUD-to-method map:
| Operation | Method | Body allowed | Success code | Idempotent |
|---|---|---|---|---|
| Read / list | GET |
no | 200 |
yes |
| Create | POST |
required | 201 + Location |
no |
| Replace | PUT |
full object | 200 or 204 |
yes |
| Partial update | PATCH |
changed fields | 200 |
not guaranteed |
| Remove | DELETE |
optional | 204 (or 200) |
yes |
Three properties in that table carry most interview questions. GET must be safe: it changes no state, so a proxy may cache it and your test may retry it freely; a body on GET is syntactically tolerated but widely ignored, so if a value matters put it in the query string — GET /orders?status=open&limit=25. POST is the only non-idempotent create: two identical POSTs legitimately produce two orders, so a duplicate-submit test must assert a business guard (a 409 conflict or an Idempotency-Key header echoing the first response), never merely the 201. PUT means replace: send only notes and you have told the server the resource is {notes: ...} — every field you omitted is legitimately dropped, which is exactly the data loss the tester saw.
PATCH sends only what changes but is not idempotent by definition: applying {"discount": 10} twice may double the discount depending on the patch dialect (JSON Merge Patch merges keys; JSON Patch applies an ordered operation list where a replace is repeatable but an add to an array may not be). "What is the difference between PUT and PATCH?" is a classic API-testing interview question — answer it on two axes, replacement versus partial and idempotent versus not, instead of "PUT sends more data."
Status classes you must classify on sight:
2xx succeeded 200 OK · 201 Created · 204 No Content
3xx redirect / cached 301 Moved Permanently · 304 Not Modified
4xx you sent a bad request 400 · 401 · 403 · 404 · 409 · 422 · 429
5xx the server broke 500 · 502 · 503 · 504
The 4xx family powers your negative tests: 400 malformed JSON, 401 no or invalid credentials, 403 authenticated but not permitted, 404 resource absent, 409 state conflict such as a duplicate email, 422 body parsed but semantically invalid (end date before start date), 429 rate limited. "401 versus 403" is the single most-asked auth question in API interviews: 401 says who are you?, 403 says I know who you are, and no.
Two habits save you later. Assert the status class first and the payload second — a server answering 200 with {"error": "not found"} is itself a defect worth filing. And treat DELETE idempotency as a contract to pin: 204 then 404 on the second call are both defensible, so write the expectation your API documents rather than assuming.
Pick one endpoint you own, write down its method, success code, and three failure codes, and provoke each from Postman. The client itself — how to organize those requests so a regression run stays reviewable — is next: Status Codes and Auth Testing. If the fixture data behind those endpoints is still fuzzy, revisit Fixtures and Test Data Layers first.
Organizing Collections by Resource So a Regression Run Reviews Clean
Open a colleague's Postman workspace and you will find forty requests named Untitled Request, GET copy, and test 2 (fixed). Nobody can run that collection with confidence, and a reviewer cannot tell what it covers. Collection structure is not cosmetic: it decides whether your API regression suite is an asset or a liability the day someone else inherits it.
A Postman collection is a JSON document (schema v2.1) of folders and requests. Postman's own guidance — and the shape that survives contact with CI — is one folder per resource, requests named by intent:
Orders API/
Create/
POST create order → 201 + Location
POST duplicate idempotency key → 409
Read/
GET list orders (status=open, limit=25) → 200
GET order 404 → 404
Update/
PUT replace notes → 200
PATCH partial discount → 200
Delete/
DELETE order → 204, then repeat → 404
Notice the naming rule: method, resource, scenario, expected result. A name like GET list orders (status=open, limit=25) → 200 tells a reviewer the request's purpose without opening it, and it shows up verbatim in the Collection Runner and Newman output, so a failure message reads as a specification breach instead of a mystery. Two naming habits matter in practice: the expectation goes in the name (→ 404) so a passing run documents the contract, and scenario-specific queries are spelled out so nobody confuses a boundary case with the happy path.
Inside each request, Postman exposes the same panels every time — Params, Authorization, Headers, Body, Settings — and using them consistently is half the structure. Put query parameters in the Params table rather than pasting them into the URL: the table serializes, encodes, and documents them, and it lets you disable one for a negative run with a checkbox. Put auth at the request level only when a single endpoint genuinely differs; otherwise set it on the collection so it inherits down. Header discipline is worth a rule: never hand-edit Content-Type when choosing a Body type — Postman sets it from the dropdown, and a stale application/x-www-form-urlencoded alongside a JSON body is one of the most common self-inflicted 400s.
Order the folders for the run, not for reading. The Collection Runner executes requests in document order, so put creation before read-before update-before delete: the run then tells one coherent story against a live server and each request's assertions can rely on state the previous request made. Keep that ordering stable and a full regression run becomes reproducible from a clean database.
Three mechanics you will use constantly: Save (an unsaved request is invisible to the runner), Examples (saved request/response pairs that become mock endpoints and documentation), and Tests (the assertions tab — a placeholder pm.test("status is 200", ...) now, real schema checks once automation arrives). Export the collection with Export → JSON and commit the file next to your tests; the workspace copy is collaboration, the committed file is the versioned artifact your pipeline runs. Put a description on every folder and on any request whose name cannot carry the full scenario — the description renders in Postman's documentation view, which becomes the fastest onboarding document your API team owns.
One structural decision causes arguments in every team: where do negative cases live? Do not scatter GET order (missing token) into the happy-path folder. Give each resource folder a Negative/ subfolder (or a top-level Auth & Errors/ collection that reuses the same variables) so a regression run can execute happy paths only on a tight PR pipeline and the full negative set on nightly. The runner's Save advanced responses and environment-override options let you configure that split once and reuse it.
Structure buys you the run; what makes the run portable across machines is variables — the next step: Automating Postman Collections follows this topic in the automation track, while the very next topic drills the codes those tests assert: Status Codes and Auth Testing.
Variables and Environments: Running One Collection Against Local, Staging, and Prod
The fastest way to break a collection is to hard-code http://localhost:3000 into forty URLs. The day you must run the same suite against staging, the file becomes a find-and-replace exercise — and a leaked production host in a commit is a security review, not a refactor. Postman solves this with a variable system whose only rule to memorize is: the value lives in a scope, the request carries a reference.
Scopes shadow each other in a fixed order — a lookup of {{baseUrl}} resolves against the first scope that defines it:
| Scope | Lifetime | Typical use |
|---|---|---|
| Local | during one request execution | scratch values from a script |
| Data | one collection run, per row | the data-driven rows covered later |
| Environment | selected via the environment picker | baseUrl, per-stage config |
| Collection | shared by the collection | resource ids, default timeouts |
| Global | everything in the workspace | rare; avoid in shared workspaces |
So the collection stores requests as {{baseUrl}}/orders, and an environment supplies the value: local → http://localhost:3000, staging → https://api.staging.acme.dev, prod → https://api.acme.com. Switching the picker in the top-right corner re-points the entire suite; in CI the same switch happens without any UI:
newman run orders.postman_collection.json -e staging.postman_environment.json \
--env-var "token=$STAGING_TOKEN"
That command line reveals the safety rules. Environment files are exported JSON; they routinely contain tokens, and they get committed. Treat them as secrets: keep non-secret values (baseUrl, page sizes) in a committed *.postman_environment.json, and inject credentials at run time with --env-var or --global-var so nothing sensitive touches disk. Postman masks values flagged as type: secret in the UI, but masking is display-deep — a console.log(pm.environment.get("token")) in a test script still prints it to the runner log.
Prefer collection variables over environment variables for anything the API itself owns. Collection variables travel with the exported file, so a colleague who imports the collection gets working defaults with no setup; environment variables are per-stage machine config. The split is simple: who owns the value — the API contract or the deployment? Contract (paths, default limit, expected enum values) → collection variable. Deployment (host, credentials, feature flags) → environment variable.
Scripts touch variables through three distinct objects, and confusing them is a common bug: pm.collectionVariables.set("orderId", "42") writes a value every subsequent request in this collection can read; pm.environment.set(...) writes to the active environment (so it persists between runs until you reset it); pm.variables.set(...) is request-local scratch. When a value mysteriously survives a run you expected to be clean, an environment variable you forgot to reset is almost always the culprit — Postman's Persist All toggle keeps environment edits across runs deliberately.
An exported environment file itself is small and worth reading once, because that is what CI consumes:
{
"id": "env-staging-42",
"name": "staging",
"values": [
{ "key": "baseUrl", "value": "https://api.staging.acme.dev", "type": "default" },
{ "key": "token", "value": "", "type": "secret" }
]
}
An empty secret-typed value is intentional: CI fills it with --env-var, and a reviewer can see at a glance that no credential lives in the file. Also set sensible disable-self-signed-certificate and follow redirects flags per environment — a staging box with a self-signed cert fails every request with an opaque SSL Error that has nothing to do with the API, and auto-following redirects can mask a 301 that should be an asserted contract detail.
Practice the loop once end to end: build {{baseUrl}}/orders, create two environments, run against local, switch to staging, and confirm no URL changed in the requests themselves. When you can do that without opening an editor, you are ready to assert on what comes back: Status Codes and Auth Testing.
Quiz
1. A request sends `PUT /orders/42` with body `{"notes": "rush"}` and the order afterwards has no customer id. What is the correct conclusion?
2. An authenticated user requests a resource belonging to another team and receives 403. What does that status tell you?
3. A UI submits an order twice after a network timeout and your API returns 201 for both. What is the correct test-design conclusion?
4. Which pair of methods must be safe (side-effect free) so a test may retry them without changing server state?
Flashcards
Question
Map CRUD to HTTP methods with the success code each returns.
Click to reveal answer
Answer
Read → GET (200); Create → POST (201 + Location header); Replace → PUT (200 or 204); Partial update → PATCH (200); Remove → DELETE (204, or 200 when returning a body).
Question
What is idempotency, and which methods are idempotent?
Click to reveal answer
Answer
Repeating a request yields the same end state as performing it once. GET, HEAD, PUT, and DELETE are idempotent; POST and (by specification) PATCH are not. Idempotency is what makes automatic retries after a timeout safe.
Question
Difference between 401 and 403 — the classic API interview question.
Click to reveal answer
Answer
401 Unauthorized: no credentials or invalid ones — authenticate first (include a WWW-Authenticate challenge). 403 Forbidden: identity is known and valid, but the policy denies the action. Same for 404 vs 403 on hidden resources: 404 hides existence, 403 acknowledges it.
Question
What do 400, 422, and 429 each mean for a test's expected error path?
Click to reveal answer
Answer
400 Bad Request: the body failed parsing (malformed JSON, wrong shape). 422 Unprocessable Entity: the body parsed but violates semantic rules (end date before start date, unknown enum value). 429 Too Many Requests: rate limit exceeded — assert the Retry-After header where the API documents it.
Question
Why does a collection store `{{baseUrl}}/orders` instead of a literal host?
Click to reveal answer
Answer
The request holds a reference; each environment supplies the value (local, staging, prod). Switching environments — in the UI or via `newman -e env.json` — re-points the whole suite with zero edits to requests, and it prevents hard-coded hosts and credentials from leaking into the committed collection.
Revision Notes
Key Takeaways
- 1. PUT replaces the whole resource and PATCH changes only what you send — that pair of axes (replace/partial × idempotent/not) answers every 'PUT vs PATCH' interview question
- 2. POST is deliberately non-idempotent; duplicate-submit protection is an application guard (409 or Idempotency-Key) that your test must assert explicitly
- 3. Assert status first, payload second — a 200 carrying `{"error":"not found"}` is itself a defect
- 4. Name requests `METHOD resource scenario → status` so Collection Runner and Newman output read like a specification
- 5. Requests reference `{{baseUrl}}`; environments own hosts and credentials, so the same collection runs on any stage without edits
Interview Tips
- • "PUT versus PATCH?" — replacement vs partial, idempotent vs not; mention JSON Merge Patch vs JSON Patch dialects and you go one level deeper than most candidates
- • "401 vs 403 vs 404?" — 401 missing/invalid identity, 403 known identity denied, 404 absent (or existence hidden by policy — state that you test which one the API chose)
- • "How do you prevent double-submit orders?" — idempotency keys sent by the client and echoed by the server, or unique-constraint conflict → 409; explain why the test asserts that mechanism rather than a single 201
- • "How would you run this suite in CI?" — exported collection JSON + environment file committed, credentials injected with `--env-var` from CI secrets, exit code of Newman gates the pipeline
Cheat Sheet
REST Fundamentals with Postman
CRUD → method → status:
| CRUD | Method | Success | Retry safe? |
|---|---|---|---|
| Read | GET | 200 | yes (safe + idempotent) |
| Create | POST | 201 + Location | never automatically |
| Replace | PUT | 200/204 | yes (idempotent) |
| Partial | PATCH | 200 | no |
| Delete | DELETE | 204 | yes (idempotent) |
Status classes: 2xx success · 3xx redirect/not-modified · 4xx caller's fault (400 parse, 401 no identity, 403 denied, 404 absent, 409 conflict, 422 semantic, 429 rate-limited) · 5xx server fault.
Collection naming: METHOD resource scenario → expected status — e.g. GET order 404 → 404. Folder per resource; order folders create → read → update → delete so a run tells one story.
Variable scopes (outermost wins): local → data → environment → collection → global. Own-the-contract → collection variable; own-the-deployment → environment variable; secrets injected via newman --env-var, never committed.
# run committed collection against a stage, secrets from CI
newman run orders.postman_collection.json -e staging.postman_environment.json --env-var "token=$API_TOKEN"