What you'll learn
Quick Answer
Build a collection per API, keep the host in a {{baseUrl}} environment variable, and store the token from your login request with pm.environment.set so nothing is pasted by hand. Write assertions in the Tests tab with pm.test and pm.expect, guarding pm.response.json() because it throws on an HTML error page. Then run the whole collection from the command line with Newman so a failed assertion turns your build red.
Sending your first request properly
Postman is an HTTP client with a memory. You pick a method, type a URL, add headers and a body, and it shows you the raw response along with status code, timing and size. The value over curl is that the request is saved, named and shareable, so the next person on the team does not have to guess what your endpoint expects.
The detail beginners get wrong is the body type. Under the Body tab, raw + JSON sends Content-Type: application/json, while form-data sends a multipart body and x-www-form-urlencoded sends key=value pairs. If you select raw but leave the dropdown on Text, Postman sends Content-Type: text/plain, Express skips it because express.json() only parses JSON bodies, and req.body arrives as an empty object. You then spend an hour debugging a server that is behaving perfectly.
POST {{baseUrl}}/api/orders
Content-Type: application/json
{
"courseId": "python-basics",
"city": "Pune",
"amount": 199
}
Use path variables rather than hardcoded ids. Typing {{baseUrl}}/api/orders/:orderId makes Postman show an editable Path Variables row, which keeps the saved request generic. Query strings typed into the URL bar appear automatically in the Params tab and vice versa, so you never have to hand-encode a space as %20.
Worth internalising early: Postman is not a browser. It does not enforce CORS, it does not automatically attach your site cookies, and it does not send a preflight OPTIONS request. So "it works in Postman but fails from my React app" is almost never a broken API. It is a missing CORS header, a cookie that needs credentials: 'include', or a request the browser blocked before it ever left.
Collections, environments and variables
A collection is a saved folder of requests. Structure it the way your API is structured, one folder per resource: Auth, Orders, Certificates. Once requests live in a collection you can run them in sequence, share them with a teammate, and commit an export next to your code so the API documentation cannot drift away from the API.
Environments are what make a collection reusable. Instead of typing http://localhost:3000 into forty requests, define baseUrl once and reference it as {{baseUrl}}. Create one environment for local, one for staging and one for production, then switch with the dropdown in the corner. The same collection now tests all three.
Postman resolves a variable from the most specific scope outward: local (set during a run) beats data file, which beats environment, which beats collection, which beats global. This ordering is the source of a maddening bug. You set token in the environment, it works, then months later someone adds a collection variable also called token, and now nobody can tell which one is being sent. Keep names distinct and prefer environment variables for anything that changes per deployment.
// in a Tests script, after login
pm.environment.set("token", pm.response.json().token);
// collection-wide state that is not deployment specific
pm.collectionVariables.set("orderId", pm.response.json().orderId);
Now the part that has leaked real credentials for real teams. Every Postman variable has two boxes: Initial Value and Current Value. The initial value is the shared one, it syncs to your workspace and travels inside exports. The current value stays local to you. People paste a live bearer token or a database password into the initial value box because it is the first field on the row, and it is then visible to everyone in the workspace and baked into any exported JSON. Secrets belong in the current value, marked as secret type, or better, never in Postman at all and injected at run time instead.
Testing auth flows without pasting tokens
The manual loop of logging in, copying a token out of the response and pasting it into an Authorization header is where most people give up on Postman. Automate it once and never think about it again. In the Tests tab of your login request, capture the token:
const body = pm.response.json();
pm.test("login returns a token", function () {
pm.expect(body.token).to.be.a("string").and.not.empty;
});
pm.environment.set("token", body.token);
pm.environment.set("tokenExpiry", Date.now() + 55 * 60 * 1000);
Then set Bearer Token auth at the collection level with the value {{token}}, and leave every individual request on "Inherit auth from parent". One place to change, and new requests are authenticated the moment you create them. If a request is mysteriously returning 401, check that its Authorization tab has not been left on "No Auth", which is the default for a freshly created request outside the collection.
For long sessions, refresh in a collection-level Pre-request Script so the token is renewed before it expires rather than halfway through a run:
const expiry = Number(pm.environment.get("tokenExpiry") || 0);
if (Date.now() > expiry) {
pm.sendRequest({
url: pm.environment.get("baseUrl") + "/api/auth/login",
method: "POST",
header: { "Content-Type": "application/json" },
body: {
mode: "raw",
raw: JSON.stringify({
email: pm.environment.get("email"),
password: pm.environment.get("password")
})
}
}, function (err, res) {
if (err) { console.log(err); return; }
pm.environment.set("token", res.json().token);
pm.environment.set("tokenExpiry", Date.now() + 55 * 60 * 1000);
});
}
The auth tests people forget are the negative ones. Send the request with no token and assert 401. Send it with a valid token belonging to user A but an orderId that belongs to user B, and assert 403 or 404. That second case catches insecure direct object reference, which is a genuine vulnerability that passes every happy-path test ever written.
Writing assertions in the Tests tab
Scripts in the Tests tab run after the response arrives. They are plain JavaScript with Postman's pm object and a Chai-style pm.expect. Each pm.test block becomes a named pass or fail line in the results panel and, more importantly, in your CI output later.
pm.test("status is 200", function () {
pm.response.to.have.status(200);
});
const contentType = pm.response.headers.get("Content-Type") || "";
pm.test("responds with JSON", function () {
pm.expect(contentType).to.include("application/json");
});
if (contentType.includes("application/json")) {
const body = pm.response.json();
pm.test("order has an id and a valid status", function () {
pm.expect(body).to.have.property("orderId").that.is.a("string");
pm.expect(body.status).to.be.oneOf(["created", "pending"]);
});
pm.collectionVariables.set("orderId", body.orderId);
}
That content-type guard is not decoration. pm.response.json() throws when the body is not JSON, and a thrown error aborts the whole script, so none of your other assertions ever report. The day your API sits behind a proxy that returns an HTML 502 page, an unguarded script shows one confusing JSON parse error instead of the honest message "status is 200 failed: expected 502 to equal 200".
Assert on the contract, not on the entire body. Comparing the full response to a fixed object means every harmless new field breaks the test and everyone starts ignoring failures. Check the fields callers depend on: the status code, the shape and type of key fields, the presence of an id, that a list is an array, that a price is a number rather than the string "199". Type assertions catch a surprising number of real serialisation bugs.
Response time assertions such as pm.expect(pm.response.responseTime).to.be.below(800) are useful on staging but noisy from a laptop on hostel wifi. Keep them out of the runs that gate a deployment unless you are testing from a stable machine.
Automating with the collection runner and Newman
The Collection Runner executes every request in a collection top to bottom with the environment you select. Because it runs in order, you can build a real flow: log in, create an order, fetch that order using the {{orderId}} the previous request saved, then cancel it. Set iterations to loop, and attach a CSV or JSON data file so each iteration supplies different inputs, referenced in the request body as {{email}} and so on.
To branch or stop, use postman.setNextRequest("Get order") inside a script, and postman.setNextRequest(null) to end the run early. Newer Postman versions also expose this as pm.execution.setNextRequest. Note that it only takes effect at the end of the current request, so anything after that line still runs.
The real payoff is running the same collection from a terminal with Newman, Postman's command line runner. Export the collection and environment as JSON, commit them next to your code, and add one command to your pipeline:
npm install -g newman
newman run api.postman_collection.json \
-e staging.postman_environment.json \
--env-var "password=$API_PASSWORD" \
--reporters cli,junit \
--reporter-junit-export newman-results.xml
Newman exits with a non-zero code when any assertion fails, which is exactly what a CI system needs to mark a build red. The --env-var flag overrides a variable at run time, which is how you keep the password out of the committed environment file and read it from a CI secret instead. Treat any exported environment JSON as public text that anyone with repository access can read.
Two habits keep these runs trustworthy. First, point them at a seeded staging database, never production, because a runner that creates orders will happily create a hundred of them. Second, make each request set up what it needs rather than depending on data a colleague created manually last month, otherwise the suite fails on a Monday for no reason anybody can reproduce and the team stops believing it.
