
Learning API testing has a chicken-and-egg problem: you need an API you’re allowed to change. Public practice APIs are mostly read-only, so you can send a GET and little else. The moment you want to create, update and delete records, you need your own.
That’s what my Postman automation guide on GitHub sets up: a small “heroes” API running on your own machine, and a Postman collection that exercises all five methods — GET, POST, PUT, PATCH and DELETE — with real assertions. Because the data is yours, you can break it and start over whenever you like.
Step 1: Start the local API
The server is a tiny Express app, nodejs-api-server. With Node.js installed:
git clone https://github.com/MRSamBadiei/nodejs-api-server.git
cd nodejs-api-server
npm i
npm run dev # prints "Running on 3000" when it's ready
It’s now answering at http://localhost:3000, and it keeps its records in data.json — copy that file somewhere first so you can reset the data later. Port 3000 already taken? On macOS or Linux run PORT=3001 npm run dev; on Windows, change PORT=3000 in the dev script in package.json.
What the API does
Knowing exactly what the server returns is what lets you write real assertions. These come straight from its code:
| Request | Success | When it fails |
|---|---|---|
GET /heros |
200 + the list of heroes | — |
GET /heros/:id |
200 + that hero | 404 Hero not found |
POST /addHero |
201 Your hero successfully added to the database. |
400 if age isn’t a number, gender isn’t male/female, or name is empty |
PATCH /heros/:id |
200 Successfuly updated. |
400 id does not exists |
PUT /heros/:id |
204 (no body) if the hero exists, 201 if it creates one | 400 on invalid data |
DELETE /heros/:id |
200 Successfuly deleted. |
400 id does not exists |
Spot anything? A missing hero is a 404 on GET but a 400 on DELETE, and the messages spell “Successfuly” with one L. Inconsistencies like these are exactly what API tests exist to catch — and your assertions have to match the real text, typos included.
Step 2: Set up the collection
- In Postman create a workspace (for example MyAutomationAPI) and a collection (APIPractice).
- Open the collection’s Variables tab and add
heroUrlwith the valuehttp://localhost:3000. Every request then starts with{{heroUrl}}, so moving to another port or server is a one-line change.
Postman has five kinds of variables — global, collection, environment, data and local. Collection variables are the right home for a base URL; globals are handy for values that one request passes to the next, as below.
Step 3: GET — your first tests
Create GET {{heroUrl}}/heros. A request only becomes a test when you check the response, so add these in the request’s Tests tab (newer Postman versions call it Scripts → Post-response):
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Content-Type is JSON", function () {
pm.response.to.have.header("Content-Type", "application/json; charset=utf-8");
});
pm.test("Date header is present", function () {
pm.response.to.have.header("Date");
});
pm.test("Returns a list", function () {
pm.expect(pm.response.json()).to.be.an("array");
});
Click Send and the Test Results tab shows each check passing or failing.
Step 4: POST with a random name — and prove it was saved
Hard-coding “sam” in every POST hides bugs and makes reruns collide. Postman can generate data with {{$randomFirstName}}, but then your test doesn’t know what name was sent. The fix is a pre-request script (Scripts → Pre-request) that picks the name first and stores it:
const name = pm.variables.replaceIn('{{$randomFirstName}}');
pm.globals.set("HERO_NAME", name);
Create POST {{heroUrl}}/addHero with this raw JSON body:
{ "name": "{{HERO_NAME}}", "age": 30, "gender": "female" }
and these tests:
pm.test("Status code is 201", function () {
pm.response.to.have.status(201);
});
pm.test("Success message", function () {
pm.expect(pm.response.json().msg).to.eql("Your hero successfully added to the database.");
});
The order of events for that request is: pre-request script → request → response → tests. That’s why the name is known before the test reads it.
This server’s POST answers with a message only, not the new record. So add one more request, GET {{heroUrl}}/heros, that finds the hero you just created and remembers its id for the next steps:
const hero = pm.response.json().find(h => h.name === pm.globals.get("HERO_NAME"));
pm.test("The new hero is in the list", function () {
pm.expect(hero).to.not.be.undefined;
});
pm.globals.set("HERO_ID", hero.id);
Step 5: PATCH, PUT and DELETE
PATCH {{heroUrl}}/heros/{{HERO_ID}}with body{ "age": 31 }— expect200and the messageSuccessfuly updated.PUT {{heroUrl}}/heros/{{HERO_ID}}with a full body — expect204, which by definition has no body to check.DELETE {{heroUrl}}/heros/{{HERO_ID}}— expect200andSuccessfuly deleted.
Then prove the delete really happened with one last GET {{heroUrl}}/heros/{{HERO_ID}}:
pm.test("The hero is gone", function () {
pm.response.to.have.status(404);
pm.expect(pm.response.json().error).to.eql("Hero not found");
});
pm.globals.unset("HERO_ID"); // tidy up for the next run
Now the collection creates, reads, changes and removes its own test data — so you can run it again and again.
Step 6: Run the whole collection from the command line and in CI
Inside Postman, the Runner plays the collection top to bottom. To run it without the app, export the collection and use Newman (or the Postman CLI):
npx newman run APIPractice.postman_collection.json
That same command is what a CI server runs on every push — start the API, run Newman, and fail the build if a test fails. Postman documents the Jenkins setup, and the same idea works in GitHub Actions.
Common problems
- “Could not send request”. The server isn’t running, or it’s on a different port than
heroUrl. - POST returns 400. Age must be a number and gender must be
maleorfemale; check the body is set to raw → JSON. - A message check fails on a correct response. Compare the text character by character — this server says “Successfuly”.
- The data is a mess. Stop the server, restore your copy of
data.json, start it again.
Frequently asked questions
Why test against a local server instead of a public API?
Because you can use every method, there are no rate limits or sign-ups, and you can reset the data whenever you want. It’s also how real teams test: against an environment they control.
Global, collection or environment variable?
Collection variables for settings that belong to the collection (the base URL), environment variables for values that change between dev, test and production, and globals or local variables for values passed between requests in one run.
Do I need a paid Postman plan?
No — everything here works on the free plan, and Newman is free and open source.
The original walkthrough, with screenshots of every step, is on GitHub. If you want UI and API tests in Java instead, see my ready-made Java test automation framework.
