WEB API's Lesson 28 – Postman | Dataplexa
Web APIs · Lesson 28

Postman: Your API Command Centre

Build, run, and automate a complete API workflow inside Postman — from your first request to a full collection with environment variables and test scripts.

Before Postman existed, testing an API meant writing a curl command in the terminal, squinting at raw JSON output, and hoping you remembered the right flags. Some developers kept a text file full of curl snippets they'd paste and edit every time. It was tedious, fragile, and impossible to share with a teammate in any useful way.

Postman changed the entire mental model. Instead of one-off commands, you build collections — organised folders of API requests that run in sequence, carry shared variables, and generate test reports automatically. Teams at companies like Twilio, Shopify, and Atlassian use Postman to standardise how every developer on a backend team calls every endpoint, from local dev all the way to production.

What Postman Actually Is

Postman is a platform for working with APIs — building requests, running them, writing test assertions, generating documentation, and orchestrating automated runs across environments. The free tier gives you nearly everything an individual developer needs.

Think of it as a browser specifically designed for APIs. A browser sends HTTP requests when you type a URL and click links. Postman lets you craft any HTTP request — any method, any headers, any body — and inspect the full response, including status codes, timing, response size, and cookies.

Postman — Concept Anatomy

Concept: API Client & Testing Platform
Type: GUI Tool (desktop + web)
Used for: Request building, Collections, Environments, Automated testing
Spec: OpenAPI import/export supported
Status: Industry-standard, 30M+ users

Core Features at a Glance

Concept What it does APIForge use case
Collections Groups of saved requests you can run individually or in sequence APIForge Backend team stores all 40+ endpoints in one shared collection
Environments Named variable sets (dev/staging/prod) swapped in without editing requests Switch from localhost:3000 to api.apiforge.io with one dropdown click
Pre-request Scripts JavaScript that runs before each request — perfect for generating tokens Auto-fetch a fresh JWT before hitting protected APIForge endpoints
Test Scripts JavaScript assertions that run after each response to validate behaviour Assert every APIForge project endpoint returns status 200 and a valid body
Collection Runner Executes every request in a collection in order, shows pass/fail report Full smoke test of APIForge API before each release deployment
Newman (CLI) Postman's command-line runner for CI/CD pipelines — no GUI required APIForge DevOps runs the collection on every GitHub Actions push

The Postman Workflow

A Postman workflow is not just "open app, type URL, hit send." When done properly, it mirrors how you'd structure actual integration tests — with environments that mirror real infrastructure and assertions that catch regressions before a human does.

APIForge — Full Postman Workflow

1. Create Workspace
2. Set Environments
3. Build Collection
4. Write Test Scripts
5. Run Collection
6. Export + Newman CI

Step 1 — Setting Up Your Workspace and Environments

A Workspace in Postman is a shared space for your team's collections, environments, and APIs. Think of it like a GitHub repository — but for API requests. The APIForge Backend team has one workspace that every engineer accesses, so there's one source of truth for how every endpoint should be called.

An Environment is a named set of variables. Instead of hardcoding https://localhost:3000 into every request, you store the base URL as a variable called {{baseUrl}}. Switch the environment from Dev to Production, and every request in the entire collection instantly targets the right server.

Environment vs Global vs Collection Variables

Postman has three variable scopes. Global variables exist across all workspaces — useful for your personal API key, but risky to share. Collection variables are scoped to one collection — good for shared constants like API version. Environment variables change per environment (dev/staging/prod) — this is where your base URLs and tokens live. When the same variable name exists in multiple scopes, environment wins over collection, which wins over global.

The APIForge Backend team needs to set up two environments: one for local development and one for their staging server. Here's exactly how those environment files look when exported as JSON — Postman can import these directly.

# WHAT: APIForge Environment config — Dev environment JSON
# Import this into Postman: Environments → Import → Paste JSON

{
  "name": "APIForge Dev",
  "values": [
    {
      "key": "baseUrl",
      "value": "http://localhost:3000",
      "type": "default",
      "enabled": true
    },
    {
      "key": "apiVersion",
      "value": "v1",
      "type": "default",
      "enabled": true
    },
    {
      "key": "authToken",
      "value": "",
      "type": "secret",
      "enabled": true
    },
    {
      "key": "teamId",
      "value": "team_abc123",
      "type": "default",
      "enabled": true
    }
  ]
}
Postman Environment Imported Successfully Environment: APIForge Dev Variables loaded: 4 baseUrl → http://localhost:3000 [active] apiVersion → v1 [active] authToken → •••••••• [secret, empty] teamId → team_abc123 [active] Usage in requests: URL field: {{baseUrl}}/api/{{apiVersion}}/projects Header value: Bearer {{authToken}} Query param: ?team={{teamId}} To switch to Staging: select "APIForge Staging" in the environment dropdown — all requests update automatically.

What just happened?

The environment JSON defines four named variables that any request in the collection can reference using the {{variableName}} syntax. Postman interpolates these at request time — the raw request still says {{baseUrl}}, but Postman sends http://localhost:3000.

The type: "secret" on authToken means Postman masks it in the UI — useful for tokens that should not appear on screen during a screenshare.

Try this: Create a second environment called "APIForge Staging" with baseUrl set to https://staging.apiforge.io. Then toggle between them and watch the URL in every request change instantly.

Step 2 — Building a Collection with Real Requests

A Collection is the core unit of Postman organisation. It is a folder — or a tree of folders — containing saved HTTP requests. You can think of it as a living document of how your API actually works, except it is executable. Every request in the collection is runnable and shareable.

The APIForge Backend team organises their collection by resource: a Projects folder, a Users folder, an Auth folder, and an Integrations folder. Inside each folder are the requests for every operation — GET, POST, PUT, DELETE — each pre-configured with headers, body, and auth.

Request-level Auth

Set auth on individual requests — useful when mixing public and protected endpoints in one collection.

Collection-level Auth

Set auth once at the collection level and every request inherits it automatically — less duplication, easier token rotation.

Folder Organisation

Group requests by resource or feature. Nested folders mirror your route structure — makes navigation fast in large APIs with 50+ endpoints.

Request Descriptions

Postman can publish your collection as documentation. Descriptions you add to requests appear as human-readable docs — no extra tooling needed.

Here is the APIForge Projects collection — four key requests that cover the full CRUD lifecycle for the /projects resource. These requests use environment variables, so they work against dev or staging without any changes.

# WHAT: APIForge Projects — four CRUD requests in Postman collection
# Using {{baseUrl}} and {{apiVersion}} from active environment

--- Request 1: List All Projects ---
GET {{baseUrl}}/api/{{apiVersion}}/projects
Headers:
  Authorization: Bearer {{authToken}}
  Accept: application/json

--- Request 2: Create Project ---
POST {{baseUrl}}/api/{{apiVersion}}/projects
Headers:
  Authorization: Bearer {{authToken}}
  Content-Type: application/json
Body (raw JSON):
{
  "name": "Customer Portal Redesign",
  "team_id": "{{teamId}}",
  "visibility": "internal",
  "tags": ["frontend", "q3-2025"]
}

--- Request 3: Get Single Project ---
GET {{baseUrl}}/api/{{apiVersion}}/projects/{{projectId}}
Headers:
  Authorization: Bearer {{authToken}}

--- Request 4: Delete Project ---
DELETE {{baseUrl}}/api/{{apiVersion}}/projects/{{projectId}}
Headers:
  Authorization: Bearer {{authToken}}
Request 1 — GET /api/v1/projects Status: 200 OK | Time: 48ms | Size: 1.24 KB { "data": [ { "id": "proj_k9x2m", "name": "Customer Portal Redesign", "team_id": "team_abc123", "visibility": "internal", "created_at": "2025-07-14T09:22:11Z" }, { "id": "proj_r4t8n", "name": "Internal API Gateway", "team_id": "team_abc123", "visibility": "private", "created_at": "2025-07-01T14:55:30Z" } ], "meta": { "total": 2, "page": 1, "per_page": 20 } } Request 2 — POST /api/v1/projects Status: 201 Created | Time: 62ms | Size: 0.48 KB { "data": { "id": "proj_w7y1z", "name": "Customer Portal Redesign", "team_id": "team_abc123", "visibility": "internal", "tags": ["frontend", "q3-2025"], "created_at": "2025-07-15T11:04:37Z" } }

What just happened?

Every URL and header in these requests uses {{variables}} rather than hardcoded values. Postman resolves them from whichever environment is active — so the entire collection works against dev, staging, or production by switching one dropdown.

Notice the POST response returned 201 Created, not 200. That is the correct status for a resource creation — Postman shows the exact status code so you can verify your API is returning semantically correct responses, not just "something that worked."

Try this: After the POST request runs and returns a new project ID, store it automatically: in the POST request's Tests tab, write pm.environment.set("projectId", pm.response.json().data.id); — now every subsequent request that uses {{projectId}} targets the newly created resource.

Step 3 — Writing Test Scripts That Actually Catch Bugs

Most developers use Postman to send a request and eyeball the response. That is fine for exploration, but it does not scale. When you have 40 endpoints and a release on Friday, "eyeballing" is not a strategy.

Postman's test scripts run JavaScript after each response. They use a built-in assertion library called pm.test() — you write human-readable test names and assertions, and Postman reports pass or fail. These tests travel with the collection. Every engineer who runs it sees the same results.

pm.test() — The Anatomy

pm.test("name", function() { pm.expect(value).to.equal(expected); }) — the first argument is the test name shown in the report. The second is a function containing one or more assertions. If any assertion throws, the test is marked failed. You can chain multiple expectations: pm.expect(body.data).to.be.an("array").that.has.lengthOf.above(0).

The APIForge Backend team writes test scripts for every endpoint in their collection. Here is the complete test suite for the Projects endpoints — real assertions that would catch real regressions.

// WHAT: APIForge — Postman test scripts for GET /projects and POST /projects
// Paste these in the "Tests" tab of each request

// ── Tests for GET /api/v1/projects ─────────────────────────────────
pm.test("Status code is 200", function () {
  pm.response.to.have.status(200);
});

pm.test("Response time is under 200ms", function () {
  pm.expect(pm.response.responseTime).to.be.below(200);
});

pm.test("Response has data array", function () {
  const body = pm.response.json();
  pm.expect(body).to.have.property("data");
  pm.expect(body.data).to.be.an("array");
});

pm.test("Each project has required fields", function () {
  const projects = pm.response.json().data;
  projects.forEach(function (project) {
    pm.expect(project).to.have.property("id");
    pm.expect(project).to.have.property("name");
    pm.expect(project).to.have.property("team_id");
    pm.expect(project).to.have.property("created_at");
  });
});

pm.test("Pagination meta is present", function () {
  const body = pm.response.json();
  pm.expect(body.meta).to.have.property("total");
  pm.expect(body.meta).to.have.property("page");
});

// ── Tests for POST /api/v1/projects ────────────────────────────────
pm.test("Status code is 201 Created", function () {
  pm.response.to.have.status(201);
});

pm.test("Created project has a generated ID", function () {
  const project = pm.response.json().data;
  pm.expect(project.id).to.be.a("string").that.is.not.empty;
});

pm.test("Returned name matches sent name", function () {
  const project = pm.response.json().data;
  pm.expect(project.name).to.equal("Customer Portal Redesign");
});

// Store the new project ID for downstream requests
const newId = pm.response.json().data.id;
pm.environment.set("projectId", newId);
console.log("Stored projectId:", newId);
GET /api/v1/projects — Test Results ───────────────────────────────────────────── PASS Status code is 200 (1ms) PASS Response time is under 200ms (48ms actual) PASS Response has data array (1ms) PASS Each project has required fields (2ms) PASS Pagination meta is present (1ms) 5 passing, 0 failing POST /api/v1/projects — Test Results ───────────────────────────────────────────── PASS Status code is 201 Created (1ms) PASS Created project has a generated ID (1ms) PASS Returned name matches sent name (1ms) 3 passing, 0 failing Environment updated: projectId = "proj_w7y1z" Console: Stored projectId: proj_w7y1z

What just happened?

Every assertion has a plain-English name. When a test fails, Postman shows exactly which assertion failed and what the actual vs expected value was — no debugging required. The test names are also the report output, so non-technical stakeholders can read the collection run report and understand what passed.

The last three lines of the POST script store the new project ID into the environment automatically. This is called chaining — each request in the collection passes data to the next, so a full CRUD flow runs end-to-end without any manual copying of IDs.

Try this: Add a test that intentionally fails — pm.expect(200).to.equal(404) — and run it. Watch how Postman reports the failure, including the exact line number. Then fix it. This is how you know your test infrastructure is actually working.

Step 4 — Pre-request Scripts for Auth Automation

JWT tokens expire. API keys rotate. If your collection depends on a static token stored in an environment variable, it will break the moment that token dies — usually at 9 AM on a Monday before a big demo.

Pre-request scripts solve this. They run before the actual request fires. You can use them to call an auth endpoint, grab a fresh token, and store it in the environment — all automatically, every time. The APIForge team puts this script on the collection level so it runs before every protected request.

// WHAT: APIForge — Collection-level pre-request script
// Automatically fetches a fresh JWT before every protected request
// Place in: Collection → Edit → Pre-request Script tab

const tokenExpiry = pm.environment.get("tokenExpiry");
const now = Date.now();

// Only fetch a new token if the current one has expired
if (!tokenExpiry || now > parseInt(tokenExpiry)) {
  const loginRequest = {
    url: pm.environment.get("baseUrl") + "/api/v1/auth/token",
    method: "POST",
    header: {
      "Content-Type": "application/json"
    },
    body: {
      mode: "raw",
      raw: JSON.stringify({
        client_id: pm.environment.get("clientId"),
        client_secret: pm.environment.get("clientSecret"),
        grant_type: "client_credentials"
      })
    }
  };

  pm.sendRequest(loginRequest, function (err, response) {
    if (err) {
      console.error("Token fetch failed:", err);
      return;
    }

    const json = response.json();
    pm.environment.set("authToken", json.access_token);

    // Store expiry — token lasts 3600 seconds (1 hour)
    const expiresAt = Date.now() + (json.expires_in * 1000);
    pm.environment.set("tokenExpiry", expiresAt.toString());

    console.log("New token fetched. Expires in:", json.expires_in, "seconds");
  });
} else {
  console.log("Token still valid. Skipping auth request.");
}
Pre-request Script Executed Console output: New token fetched. Expires in: 3600 seconds Environment updated: authToken → eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... [masked] tokenExpiry → 1752674677000 Actual request fired: GET http://localhost:3000/api/v1/projects Headers: Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... Accept: application/json Response: 200 OK — 2 projects returned ─── On next request (within 1 hour) ────────────────── Console output: Token still valid. Skipping auth request. (No extra auth call made — 0ms overhead)

What just happened?

The script checks the stored expiry timestamp before making any auth call. If the token is still valid, it skips the login request entirely — zero extra HTTP round trips. If it has expired, it fires a pm.sendRequest() call (Postman's way of making requests inside scripts), stores the fresh token, and records the new expiry time.

Because this script lives at the collection level, it runs before every single request in the collection automatically. Engineers on the APIForge team never manually copy-paste tokens into Postman again.

Try this: Set tokenExpiry to 0 in your environment manually, then run any request. Watch the console log — it should say "New token fetched" and fire the auth request before the main one.

Step 5 — Running the Full Collection

The Collection Runner is Postman's built-in test executor. Open it, select your collection, pick an environment, set the number of iterations, and click Run. Postman fires every request in sequence, runs all test scripts, and produces a report showing exactly which tests passed and which failed.

The APIForge Backend team runs their full collection before every release. It covers authentication, project CRUD, user management, and integration endpoints — 23 requests, 71 test assertions, completing in under 8 seconds against their staging server.

Without Collection Runner

Developer manually sends 23 requests before release, checking responses by eye.

Takes 20–30 minutes. Misses edge cases. No written record of what was tested.

If a bug is present, it may only surface in production — hours or days later.

With Collection Runner

23 requests, 71 assertions run in 7.8 seconds. Full pass/fail report generated automatically.

Every engineer on the team runs the same test suite. Results are consistent and repeatable.

Regressions caught before deployment. Report exported as HTML and attached to the release PR.

Step 6 — Automating with Newman in CI/CD

Postman's GUI is great for humans. But a CI/CD pipeline has no screen — it needs a command-line tool. That tool is Newman. Newman is Postman's official CLI runner. You export your collection and environment as JSON files, commit them to your repository, and Newman runs them in any shell — GitHub Actions, Jenkins, CircleCI, anywhere.

The APIForge DevOps team added Newman to their GitHub Actions pipeline. Every pull request triggers a full collection run against the staging API. If any test fails, the PR is blocked from merging. This is the same pattern Stripe and GitHub use internally to protect their APIs.

# WHAT: APIForge — Newman in GitHub Actions CI/CD pipeline
# File: .github/workflows/api-tests.yml

name: API Integration Tests

on:
  pull_request:
    branches: [main, staging]

jobs:
  api-tests:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Install Newman and HTML reporter
        run: |
          npm install -g newman
          npm install -g newman-reporter-htmlextra

      - name: Run APIForge collection against staging
        run: |
          newman run ./postman/apiforge-collection.json \
            --environment ./postman/apiforge-staging.json \
            --reporters cli,htmlextra \
            --reporter-htmlextra-export ./reports/api-test-report.html \
            --bail
        env:
          CLIENT_SECRET: ${{ secrets.APIFORGE_CLIENT_SECRET }}

      - name: Upload test report as artifact
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: api-test-report
          path: ./reports/api-test-report.html
GitHub Actions — api-tests job Step: Install Newman and HTML reporter + newman@6.1.0 installed globally + newman-reporter-htmlextra@1.22.11 installed Step: Run APIForge collection against staging newman APIForge Backend API Auth POST Get Token [200 OK, 312ms] Projects GET List Projects [200 OK, 48ms] POST Create Project [201 Created, 62ms] GET Get Project by ID [200 OK, 41ms] PUT Update Project [200 OK, 57ms] DELETE Delete Project [204 No Content, 39ms] Users GET List Team Members [200 OK, 44ms] POST Invite Member [201 Created, 88ms] ┌─────────────────────────┬──────────┬──────────┐ │ │ executed │ failed │ ├─────────────────────────┼──────────┼──────────┤ │ iterations │ 1 │ 0 │ │ requests │ 23 │ 0 │ │ test-scripts │ 46 │ 0 │ │ prerequest-scripts │ 23 │ 0 │ │ assertions │ 71 │ 0 │ ├─────────────────────────┼──────────┼──────────┤ │ total run duration: 7.8s│ │ │ └─────────────────────────┴──────────┴──────────┘ Report saved: ./reports/api-test-report.html Job status: SUCCESS — PR cleared for merge review

What just happened?

Newman ran the full collection against staging and reported 71 assertions across 23 requests — all passing — in under 8 seconds. The --bail flag tells Newman to stop immediately on the first failure, which prevents a cascading run where early failures corrupt downstream request state.

The HTML report is uploaded as a GitHub Actions artifact — anyone on the team can download the report and see which specific assertions passed, response times, and request/response bodies for every call. No terminal access required.

Try this: Export your Postman collection as JSON (Collection → ... → Export → Collection v2.1) and run it locally with newman run collection.json. If Newman is not installed, run npm install -g newman first.

Postman Sync vs Exported JSON — Which Should You Commit?

Postman Cloud Sync

Collections live in Postman's cloud workspace. Newman uses your Postman API key and the collection UID to pull the latest version at runtime.

Pros: Always runs the latest version, no manual export. Cons: Requires API key in CI, breaks if Postman is down.

Exported JSON in Git

Collection and environment JSONs are committed to the repository. Newman reads local files. No external dependency at run time.

Pros: Version-controlled alongside code, works offline, no third-party dependency in CI. Cons: Engineers must remember to re-export after changes.

The APIForge team chose exported JSON in Git. The reason: their CI pipeline should never depend on an external SaaS being available. If Postman's cloud is unreachable during a deployment, a sync-based approach blocks every release. Local JSON files are zero-dependency — the tests always run, no matter what.

When Tests Fail — The Debugging Flow

A failed test in Postman is not a dead end — it is a breadcrumb trail. Postman gives you the request that failed, the response body, the exact assertion that threw, and the actual vs expected values. Here is the systematic way the APIForge team handles a failing CI run.

APIForge — Debugging a Newman Failure

1

Read the Newman output — it names the exact request and the exact assertion that failed, along with actual and expected values.

2

Open the request in Postman GUI and send it manually against staging — see the raw response body to understand what the server is actually returning.

3

Check the Console tab in Postman — pre-request and test scripts log output there, so you can see if a token failed to fetch or a variable was undefined.

4

Determine: is the test wrong (API behaviour changed intentionally) or is the API wrong (regression). Update whichever is incorrect.

5

Fix the issue, re-export the collection JSON if test scripts changed, push to the branch, and let CI re-run the collection automatically.

6

All 71 assertions green — PR approved for merge. The HTMLExtra report is attached to the GitHub Actions run as evidence.

Common Postman Mistake: Storing Secrets in Collections

Never hardcode API keys, tokens, or passwords into a request's headers or body — especially if the collection is shared or committed to a repository. Always use environment variables with the type: "secret" flag for sensitive values. In CI, pass secrets via environment variables from your pipeline's secrets store, not from the committed environment JSON file.

Postman vs the Alternatives

Tool Best for Collections CI/CD Free tier
Postman Teams, full workflow, scripts Yes Newman CLI Yes (3 users)
Hoppscotch Quick browser-based testing Yes Limited Yes (self-host)
Insomnia Individual developers, privacy Yes Inso CLI (limited) Yes
curl One-off requests, scripting No Manual scripting only Yes (built-in)

Postman wins on team workflows and automation. curl wins on speed for one-off checks in the terminal. Many backend engineers use both — curl to quickly poke an endpoint, Postman to build the proper test collection around it once the API is confirmed working.

Quiz

1. The APIForge Backend team adds a pre-request script at the collection level. What does this script do when a developer runs the "Get Project by ID" request?

2. The APIForge DevOps team wants to run the Postman collection in GitHub Actions and stop immediately if any single test fails. Which Newman command achieves this?

3. An APIForge engineer runs the collection in sequence: POST creates a project, then GET retrieves it by ID. The GET fails because the projectId variable is empty. What is the correct fix?

Up Next
Monitoring & Logging
The APIForge DevOps team instruments their live API with structured logs, request tracing, and uptime alerts so failures surface in seconds — not support tickets.