Back to All Articles
API Testing

Advanced Postman — Newman, Mock Servers & Monitors

Honnesh Muppala May 5, 2026 14 min read

Beyond the Basics

Most developers and QA engineers know Postman as a graphical tool for sending HTTP requests and inspecting responses. But Postman has evolved into a complete API development and testing platform capable of running fully automated test suites in CI/CD pipelines, simulating backend services that do not exist yet, monitoring production API health, and orchestrating complex multi-step API workflows.

This guide covers the advanced features that transform Postman from a request-sending GUI into a serious API automation platform: Newman CLI for headless execution, mock servers for frontend development without a backend, monitors for production uptime checks, and pre-request scripts for dynamic authentication. These are the features I rely on daily in API-heavy QA workflows.

From Experience at Virtusa: When I joined a project that had no API test automation at all, Postman was the fastest path to meaningful coverage. Within two weeks I had a collection with 80+ requests, full environment switching between dev/staging/prod, token refresh pre-request scripts, and Newman running in our Jenkins pipeline. The team went from zero automated API testing to CI-integrated coverage without introducing a new language or framework. Postman's low barrier to entry is a genuine advantage in fast-moving projects.

Architecture Overview

Understanding how Postman components relate helps you design test collections that work both interactively and in automated pipelines.

Postman
Collections
Newman CLI
(headless runner)
CI Pipeline
(GitHub Actions / Jenkins)
Scheduled Monitor
(Postman cloud)
API Under Test
Report
(HTML / JUnit / JSON)

Collections are the central artifact — they store requests, test scripts, and pre-request scripts. Newman runs collections headlessly for CI integration. Postman Monitors run collections on a schedule in Postman's cloud. Both feed into reporting systems for pass/fail tracking.

Environments and Variables

The most important Postman feature for real-world test automation is the environment system. Instead of hardcoding URLs and credentials into your requests, you reference variables using double-curly-brace syntax.

// In request URL field
{{baseUrl}}/api/v1/users/{{userId}}

// In request headers
Authorization: Bearer {{token}}

// In request body (JSON)
{
    "username": "{{testUsername}}",
    "password": "{{testPassword}}"
}

Variable scopes (highest to lowest precedence)

Scope Where Set Survives Session Best For
Local pm.variables.set() in script No — current run only Temporary values within a request chain
Data CSV/JSON data file (Collection Runner) No — per iteration Data-driven test iterations
Environment Environment panel, pm.environment.set() Yes — within environment Base URLs, auth tokens, stage-specific config
Collection Collection variables panel, pm.collectionVariables.set() Yes — within collection Shared IDs and state across the collection
Global Global panel, pm.globals.set() Yes — across all workspaces Rarely — use environment scope instead

Setting variables in test scripts

// In Tests tab — extract response data and store for next request
const responseJson = pm.response.json();

// Store user ID from create-user response for use in subsequent requests
pm.environment.set("createdUserId", responseJson.data.id);

// Store a value in collection scope for cross-request sharing
pm.collectionVariables.set("orderId", responseJson.order.id);

Pre-Request Scripts

Pre-request scripts run before the actual request is sent. The most powerful use case is dynamic authentication token refresh — instead of manually updating your token when it expires, a pre-request script fetches a fresh token and injects it automatically before every request.

// Pre-Request Script — Token refresh before each request
const tokenUrl = pm.environment.get("authBaseUrl") + "/oauth/token";
const clientId = pm.environment.get("clientId");
const clientSecret = pm.environment.get("clientSecret");

// Check if token is still valid (stored with expiry timestamp)
const tokenExpiry = pm.environment.get("tokenExpiry");
const now = Date.now();

if (!tokenExpiry || now >= parseInt(tokenExpiry)) {
    // Token missing or expired — fetch a new one
    pm.sendRequest({
        url: tokenUrl,
        method: "POST",
        header: {
            "Content-Type": "application/x-www-form-urlencoded"
        },
        body: {
            mode: "urlencoded",
            urlencoded: [
                { key: "grant_type",    value: "client_credentials" },
                { key: "client_id",     value: clientId },
                { key: "client_secret", value: clientSecret },
                { key: "scope",         value: "read write" }
            ]
        }
    }, function(err, response) {
        if (!err && response.code === 200) {
            const body = response.json();
            pm.environment.set("token", body.access_token);

            // Store expiry: current time + expires_in seconds (converted to ms)
            const expiryMs = now + (body.expires_in - 60) * 1000; // 60s buffer
            pm.environment.set("tokenExpiry", expiryMs.toString());
        } else {
            console.error("Token refresh failed:", err || response.text());
        }
    });
}

This script runs before every request in the collection. It checks if the stored token has expired. If so, it fires a synchronous pm.sendRequest() to fetch a new token, stores it in the environment, and records the expiry timestamp. The actual request then uses {{token}} in its Authorization header, which now resolves to the freshly fetched token.

Test Assertions

Postman's test scripts use the pm.test() function with Chai-style assertions via pm.expect(). Here is a complete set of test assertions covering the most common API test scenarios:

// =========================================
// Status code checks
// =========================================
pm.test("Status is 200 OK", () => {
    pm.response.to.have.status(200);
});

pm.test("Status is in 2xx range", () => {
    pm.expect(pm.response.code).to.be.within(200, 299);
});

// =========================================
// Response time
// =========================================
pm.test("Response time is under 500ms", () => {
    pm.expect(pm.response.responseTime).to.be.below(500);
});

// =========================================
// JSON body assertions
// =========================================
const body = pm.response.json();

pm.test("Response has required fields", () => {
    pm.expect(body).to.have.property("id");
    pm.expect(body).to.have.property("email");
    pm.expect(body).to.have.property("createdAt");
});

pm.test("Email matches expected format", () => {
    pm.expect(body.email).to.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
});

pm.test("User is active", () => {
    pm.expect(body.status).to.equal("active");
});

pm.test("Items array is not empty", () => {
    pm.expect(body.items).to.be.an("array").that.is.not.empty;
});

// =========================================
// JSON Schema validation
// =========================================
const Ajv = require("ajv");
const ajv = new Ajv();

const userSchema = {
    type: "object",
    required: ["id", "email", "firstName", "lastName", "status"],
    properties: {
        id:        { type: "integer" },
        email:     { type: "string" },
        firstName: { type: "string" },
        lastName:  { type: "string" },
        status:    { type: "string", enum: ["active", "inactive", "pending"] }
    },
    additionalProperties: false
};

pm.test("Response matches user schema", () => {
    const valid = ajv.validate(userSchema, body);
    pm.expect(valid, ajv.errorsText()).to.be.true;
});

// =========================================
// Header checks
// =========================================
pm.test("Content-Type is JSON", () => {
    pm.expect(pm.response.headers.get("Content-Type"))
        .to.include("application/json");
});

pm.test("Response has X-Request-ID header", () => {
    pm.expect(pm.response.headers.get("X-Request-ID")).to.not.be.null;
});

Collection Runner

The Collection Runner executes an entire Postman collection in sequence, with configurable data files for data-driven testing. Access it from the collection's context menu or the Runner tab.

Data-driven testing with CSV

Create a CSV file where each row is one test iteration:

username,password,expectedStatus,expectedRole
admin@example.com,Admin123!,200,admin
user@example.com,User456!,200,standard
invalid@example.com,wrongpass,401,
locked@example.com,Pass789!,403,

In your test script, reference the data file columns as variables:

// Test script using data-file variables
pm.test(`Login returns ${pm.iterationData.get("expectedStatus")}`, () => {
    pm.response.to.have.status(parseInt(pm.iterationData.get("expectedStatus")));
});

if (pm.response.code === 200) {
    pm.test("Role matches expected", () => {
        const body = pm.response.json();
        pm.expect(body.user.role).to.equal(pm.iterationData.get("expectedRole"));
    });
}

In Collection Runner, select your CSV file, set iteration count to match row count, and run. Each row becomes one complete pass through the collection with that row's variable values.

Newman CLI

Newman is Postman's official command-line collection runner. It runs Postman collections without the GUI — essential for CI pipeline integration.

# Install Newman globally
npm install -g newman

# Basic run — collection file + environment file
newman run collection.json -e environment.json

# With data file for data-driven testing
newman run collection.json -e environment.json -d test-data.csv

# Set iteration count
newman run collection.json -e environment.json -n 3

# Set request timeout (ms)
newman run collection.json -e environment.json --timeout-request 5000

# Fail on first error (useful for CI fast-fail)
newman run collection.json -e environment.json --bail

# Verbose output for debugging
newman run collection.json -e environment.json --verbose

Newman exit codes for CI

Newman exits with code 0 on success and non-zero on failure:

CI systems (GitHub Actions, Jenkins, CircleCI) automatically fail the build step if the process exits non-zero, so Newman integrates naturally without extra configuration.

Newman HTML Reporter

The default Newman output is console text. The newman-reporter-htmlextra package produces a rich HTML report with charts, request/response details, and failure highlights.

# Install htmlextra reporter
npm install -g newman-reporter-htmlextra

# Run with HTML report output
newman run collection.json \
    -e environment.json \
    --reporters cli,htmlextra \
    --reporter-htmlextra-export reports/newman-report.html \
    --reporter-htmlextra-title "API Test Results — Sprint 42" \
    --reporter-htmlextra-showOnlyFails

The --reporter-htmlextra-showOnlyFails flag produces a report that highlights only failing tests — useful for a quick failures-only view in large collections.

Newman in GitHub Actions

# .github/workflows/api-tests.yml
name: API Tests — Newman

on:
  push:
    branches: [main, develop]
  pull_request:

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

    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

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

      - name: Run API tests
        env:
          API_BASE_URL: ${{ secrets.API_BASE_URL }}
          API_KEY: ${{ secrets.API_KEY }}
        run: |
          # Inject environment variables into Postman environment file
          envsubst < postman/environment-template.json > postman/environment.json

          newman run postman/collection.json \
            -e postman/environment.json \
            --reporters cli,htmlextra,junit \
            --reporter-htmlextra-export reports/api-report.html \
            --reporter-junit-export reports/results.xml \
            --bail

      - name: Upload test reports
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: api-test-reports
          path: reports/

      - name: Publish test results
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: Newman API Tests
          path: reports/results.xml
          reporter: java-junit

Mock Servers in Postman

Mock servers let you simulate API responses before the real backend exists. This is invaluable for frontend teams that need an API to develop against while the backend is still being built.

Creating a mock from a collection

  1. In your collection, add example responses to each request (right-click request → Add Example)
  2. Go to the collection's "..." menu → Mock Collection
  3. Name the mock server and choose the environment
  4. Postman generates a mock URL: https://<mock-id>.mock.pstmn.io

How mock matching works

When a request hits the mock URL, Postman matches it against your saved examples by:

  1. HTTP method (GET, POST, etc.)
  2. URL path
  3. Query parameters (optional)
  4. Request headers (optional)
  5. Request body (optional)

The best-matching example's response is returned. You can save multiple examples per request to simulate different scenarios — success, not found, validation error — and the mock returns the matching one based on query parameters or headers.

// Using the mock URL in your frontend or test
const response = await fetch(
    "https://abc123.mock.pstmn.io/api/v1/users/42",
    {
        headers: {
            "x-api-key": "your-postman-api-key",
            "x-mock-match-request-body": "true"
        }
    }
);
From Experience at Amazon: On an internal tooling project, the backend team was three sprints behind the frontend team. Rather than blocking frontend progress, we created a Postman mock server from the agreed API spec. The frontend team developed and tested against the mock for six weeks. When the real backend was ready, switching from mock URL to real URL in the Postman environment revealed exactly four integration issues — all minor — versus the dozens we expected from a full cold integration. Contract clarity enforced via the mock spec was the key.

Postman Monitors

Monitors run your Postman collection on a scheduled basis from Postman's cloud infrastructure — checking that your live API is behaving correctly without you running anything locally. They are the simplest form of production API monitoring available.

Setting up a monitor

  1. In your workspace, click "Monitor Collection" from the collection menu
  2. Set the schedule: every 5 minutes, hourly, daily — down to 1-minute intervals on paid plans
  3. Choose the environment (your production environment variables)
  4. Configure notification: email on failure, Slack webhook, PagerDuty integration

What to monitor with Postman Monitors

Postman Flows

Postman Flows is a visual, no-code/low-code interface for building API workflows. You drag-and-drop blocks representing API requests, conditional logic, data transformation, and loops — connecting them with wires that represent data flow.

Flows replaces complex pre-request and test script chains for orchestration logic. Instead of writing JavaScript to extract a user ID from one response and inject it into the next request, you visually connect the output of the first block to the input of the second. Flows is particularly useful for:

Tool Comparison

Feature Postman Insomnia Bruno REST Client (VS Code)
GUI Full-featured desktop app + web Desktop app Desktop app (open source) VS Code extension only
CLI Runner Newman (excellent) Inso (limited) Bruno CLI (growing) None
Team Collaboration Cloud workspaces, versioning Git sync, cloud Git-native (files in repo) Via Git (plain .http files)
Mock Servers Built-in (cloud) Yes (Kong Studio) No No
Monitors Built-in (cloud, paid for <5min) No No No
Test Scripting JavaScript (full pm.* API) JavaScript (limited) JavaScript (growing) No
Pricing Free tier; $14/user/mo (Basic) Free (OSS); paid cloud sync Free (OSS) Free

Best Practices

1. Always use environments for base URLs — never hardcode

Every URL in your collection should be {{baseUrl}}/path/to/resource. Switching from dev to staging to production should be a single environment dropdown change — not a find-and-replace across 50 requests.

2. Put token refresh in a collection-level pre-request script, not in individual requests

If you copy the token refresh pre-request script into each request individually, you will inevitably miss some requests and have inconsistent behavior. Place it in the collection's Pre-request Script tab (accessible from the collection's Edit panel) to apply it automatically to every request in the collection.

3. Keep test scripts in the collection, not scattered in individual requests

Store common assertion helpers in collection-level scripts and call them from individual request tests. This avoids duplicating assertion logic across dozens of requests. When an assertion pattern changes, you update it in one place.

4. Export and version-control your collections

Export your Postman collection as JSON and commit it to your repository alongside your application code. This ensures the collection evolves with the API and team members can import it without syncing through Postman cloud accounts.

project/
├── postman/
│   ├── User-Service.postman_collection.json
│   ├── Order-Service.postman_collection.json
│   ├── dev.postman_environment.json
│   └── staging.postman_environment.json

5. Use folder structure within collections

Organize requests into folders by resource or feature: Authentication, Users, Orders, Products. This makes the Collection Runner's folder-level execution useful — you can run only the Authentication folder to smoke-test auth before running the full suite.


Back to Blog
From Experience — Virtusa: At Virtusa, Postman was our primary API testing tool across every client engagement. We built environment-variable-driven collections covering dev, staging, and production, with pre-request scripts handling OAuth token refresh automatically. The discipline that made the biggest difference: validating response schemas, not just status codes. This caught three breaking backend changes before they reached QA — each time, a field was renamed or a type changed. Status 200 with wrong data is a silent failure that status-code-only tests never catch.