API Testing with Postman + Newman
Build a small REST collection in the Postman desktop app, save it as JSON, then run it headless from the command line with Newman.
1 Goal
By the end of this exercise you will have a four-request Postman collection targeting jsonplaceholder.typicode.com — a free public REST API for testing. Each request will have automated assertions (status code, response shape, latency). You will export the collection to a JSON file, then run it without opening Postman at all using Newman, Postman's free CLI runner. That last step is the one that matters: your CI pipeline does not have a UI, so your API tests must run headless.
Time: about 30 minutes. Free tools only.
2 Install Postman & Newman
You need two separate things: the Postman desktop app (for authoring) and the Newman CLI (for running headless). Both are free.
-
Install the Postman desktop app
Download from postman.com/downloads. The free tier is all you need; no account is required to author or run collections locally, though signing in with a free account enables sync.
Run the
.exeinstaller. It installs to%LOCALAPPDATA%\Postman. Launch Postman and click Skip and go to the app on the sign-in screen.Drag the app into
/Applications. First launch may ask for permission to access the keychain — you can safely say no for this exercise.Use the
.tar.gzdownload, or on Ubuntu/Debian install the snap:sudo snap install postman
-
Install Node.js if you have not already
Newman runs on Node.js. If you completed Practice 01, you are set. Otherwise grab the LTS installer from nodejs.org/en/download and verify:
Expect
node --version npm --version
v20.11.xor newer for Node, and10.xor newer for npm. -
Install Newman globally via npm
npm install -g newman
Run the install in an elevated PowerShell (right-click → Run as administrator) if you hit a permissions error.
If you get
EACCES, prependsudo. Long-term, a better fix is to point npm's global prefix at a folder you own (npm config set prefix ~/.npm-global).Same
EACCESadvice. Using a Node version manager (nvm, fnm) avoids sudo entirely. -
Verify Newman
You should see something like
newman --version
6.2.1. If the command is not found, close and reopen the terminal — npm's globalbinfolder needs to be on your PATH.
3 Project setup
Create a folder to hold your exported collection and, later, the Newman run report.
mkdir api-tests cd api-tests
Final tree you are working towards:
Now open Postman and create a new collection:
-
Open Postman and create a workspace
Left sidebar → Workspaces → Create Workspace. Name it
bootcamp, visibility Personal. Click Create. -
Create a collection
Left sidebar → Collections tab → click the + button. Rename the collection to
JSONPlaceholder(double-click the name, type, press Enter). -
Add a collection-level variable for the base URL
Click the collection name → Variables tab. Add one row:
Click Save. You will reference this as
VARIABLE INITIAL VALUE CURRENT VALUE baseUrl https://jsonplaceholder.typicode.com https://jsonplaceholder.typicode.com
{{baseUrl}}in every request. Changing it in one place reconfigures the whole collection — the same principle as the Page Object Model for APIs.
4 Build the collection
Add four requests to the JSONPlaceholder collection. For each one: right-click the collection → Add request, set the method and URL, then paste the test script into the Tests tab.
Request 1: GET all posts
Method: GET · URL: {{baseUrl}}/posts
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Response is an array of 100 posts", function () {
const body = pm.response.json();
pm.expect(body).to.be.an("array");
pm.expect(body).to.have.lengthOf(100);
});
pm.test("Response time is under 2 seconds", function () {
pm.expect(pm.response.responseTime).to.be.below(2000);
});Request 2: GET a single post
Method: GET · URL: {{baseUrl}}/posts/1
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Post has the expected shape", function () {
const body = pm.response.json();
pm.expect(body).to.have.property("userId");
pm.expect(body).to.have.property("id", 1);
pm.expect(body).to.have.property("title").that.is.a("string");
pm.expect(body).to.have.property("body").that.is.a("string");
});
pm.test("Content-Type is JSON", function () {
pm.response.to.have.header("Content-Type");
pm.expect(pm.response.headers.get("Content-Type")).to.include("application/json");
});Request 3: POST a new post
Method: POST · URL: {{baseUrl}}/posts · Headers: Content-Type: application/json
Body tab → raw → dropdown set to JSON. Paste:
Body{
"title": "Bootcamp practice",
"body": "Posted from Newman",
"userId": 1
}pm.test("Status code is 201 Created", function () {
pm.response.to.have.status(201);
});
pm.test("Response echoes the title we sent", function () {
const body = pm.response.json();
pm.expect(body.title).to.eql("Bootcamp practice");
pm.expect(body.userId).to.eql(1);
});
pm.test("Response includes a new id", function () {
const body = pm.response.json();
pm.expect(body.id).to.be.a("number");
pm.collectionVariables.set("createdPostId", body.id);
});The final line stores the new post's id in a collection variable so the next request can reuse it.
Request 4: GET the post we just created
Method: GET · URL: {{baseUrl}}/posts/{{createdPostId}}
pm.test("Status code is 200 or 404 (see note)", function () {
pm.expect([200, 404]).to.include(pm.response.code);
});
pm.test("If 200, the response has our id", function () {
if (pm.response.code === 200) {
const body = pm.response.json();
pm.expect(body.id).to.eql(Number(pm.collectionVariables.get("createdPostId")));
}
});Export the collection
-
Click the three-dot menu next to the collection name → Export
Choose Collection v2.1, click Export, and save to your
api-tests/folder asjsonplaceholder.postman_collection.json. -
Sanity-check the file
Open it in a text editor. You should see JSON with an
itemarray containing your four requests and theireventblocks (that is where Postman stores your test scripts).
That JSON file is now a portable, version-controllable artifact. Commit it alongside your code. Any teammate with Newman can run it without Postman.
5 Run & verify
First, run it inside Postman to make sure your assertions behave. Click the collection → Run button → Run JSONPlaceholder. You should see all eleven assertions pass (green).
Now the important step — run it from the command line with Newman:
newman run jsonplaceholder.postman_collection.json
Expected output (abbreviated):
newman
JSONPlaceholder
→ GET all posts
GET https://jsonplaceholder.typicode.com/posts [200 OK, 26.1kB, 230ms]
✓ Status code is 200
✓ Response is an array of 100 posts
✓ Response time is under 2 seconds
→ GET a single post
GET https://jsonplaceholder.typicode.com/posts/1 [200 OK, 480B, 95ms]
✓ Status code is 200
✓ Post has the expected shape
✓ Content-Type is JSON
→ POST a new post
POST https://jsonplaceholder.typicode.com/posts [201 Created, 420B, 110ms]
✓ Status code is 201 Created
✓ Response echoes the title we sent
✓ Response includes a new id
→ GET the post we just created
GET https://jsonplaceholder.typicode.com/posts/101 [404 Not Found, 180B, 85ms]
✓ Status code is 200 or 404 (see note)
✓ If 200, the response has our id
┊┊┊┊┊┊┊┊┊┊┊┊┊┊┊┊┊┊┊┊┊
executed failed
iterations 1 0
requests 4 0
test-scripts 8 0
prerequest-scripts 4 0
assertions 11 0Eleven assertions, zero failures. That is what passing looks like.
Bonus: generate an HTML report
npm install -g newman-reporter-htmlextra newman run jsonplaceholder.postman_collection.json -r htmlextra --reporter-htmlextra-export newman-report.html
Open newman-report.html in a browser. You get a full timeline, request/response bodies, and assertion status — this is the report you attach to a CI build.
newman run path/to/collection.json. A non-zero exit code fails the build. No browser, no display server, no licences.
6 Troubleshooting
Error: "newman: command not found" (or "not recognized" on Windows)
npm's global bin directory is not on your PATH. Three fixes, in order:
- Close and reopen the terminal — PATH only updates for new sessions.
- Run
npm config get prefixand add<prefix>/bin(macOS/Linux) or<prefix>(Windows) to PATH manually. - Run Newman via
npxinstead:npx newman run collection.json. It works without global install.
Postman's Run button passes, but Newman fails with "Could not get any response"
Postman the desktop app uses your OS proxy settings by default; Newman does not. If you are on a corporate network, try:
newman run collection.json --insecure --ssl-client-cert-list none
If your proxy needs auth, pass it explicitly:
newman run collection.json --proxy http://user:pass@proxy.corp:8080
For this exercise against a public API, the simplest workaround is to run it off the corporate VPN.
"TypeError: pm.expect(...).to.have.lengthOf is not a function"
You are on a very old Newman. Postman's test library (Chai) shipped lengthOf years ago. Upgrade:
npm install -g newman@latest
Aim for Newman 6.x or newer.
Request 3 succeeds but createdPostId is empty in Request 4
Two common causes:
- The
pm.collectionVariables.set(...)line ran inside anifblock that was skipped because an earlier assertion failed. Open the Postman Console (View menu → Show Postman Console) to see the actual flow. - You set an environment variable instead of a collection variable, or vice versa. They are different scopes. Use
pm.collectionVariableson both sides (setter and{{createdPostId}}lookup).
7 Challenge
Add environments and data-driven runs
Easy: Add a fifth request — DELETE {{baseUrl}}/posts/1 — with assertions for a 200 status code and an empty response body. Re-export the collection and run it with Newman. All 13 assertions should pass.
Harder: Create a Postman environment with baseUrl pointing at reqres.in (another free public API) and adjust one request to hit {{baseUrl}}/api/users/2. Export the environment to reqres.postman_environment.json. Run it with:
newman run collection.json -e reqres.postman_environment.json
Then make the collection data-driven: create a users.csv with a userId column, reference {{userId}} in a request URL, and run:
newman run collection.json -d users.csv
Newman will iterate the collection once per CSV row — that's true data-driven API testing from the command line.