Design Postman collections with environment variables, write test scripts, chain requests to test workflows, and run collections in CI with Newman.
Postman is the most widely used API testing tool, combining a GUI for manual exploration with scripting capabilities for automated assertions. Newman is Postman's CLI runner that executes collections in CI pipelines. Together they enable both developer exploration and automated regression testing from the same collection.
This lesson covers the full Postman workflow: designing collections, writing test scripts, chaining requests, and integrating with CI via Newman.
1# Recommended Postman collection structure:
2DeveloperBootstrap API
3├── Auth
4│ ├── POST Login
5│ ├── POST Register
6│ └── POST Logout
7├── Courses
8│ ├── GET List Courses
9│ ├── POST Create Course (Admin)
10│ ├── GET Get Course by ID
11│ ├── PATCH Update Course
12│ └── DELETE Delete Course
13└── Progress
14 ├── POST Enroll
15 └── GET My Progress1// Postman Environment Variables:
2// base_url: http://localhost:3000
3// admin_email: [email protected]
4// admin_password: AdminPass123!
5// access_token: (set dynamically by Login request)
6// course_id: (set dynamically by Create Course)
7
8// Pre-request script (set in Collection > Pre-request Script):
9// Ensures base_url always has a trailing path component ready
10if (!pm.environment.get('base_url')) {
11 pm.environment.set('base_url', 'http://localhost:3000');
12}Environment files (import/export in Postman):
1{
2 "name": "Local Development",
3 "values": [
4 { "key": "base_url", "value": "http://localhost:3000", "enabled": true },
5 { "key": "admin_email", "value": "[email protected]", "enabled": true },
6 { "key": "admin_password", "value": "AdminPass123!", "enabled": true }
7 ]
8}1// POST /auth/login — Login Request
2// Tests tab:
3
4pm.test('Status is 200', () => {
5 pm.expect(pm.response.code).to.eql(200);
6});
7
8pm.test('Response has accessToken', () => {
9 const body = pm.response.json();
10 pm.expect(body).to.have.property('accessToken');
11 pm.expect(body.accessToken).to.be.a('string');
12 pm.expect(body.accessToken.length).to.be.greaterThan(50);
13});
14
15pm.test('Response time under 500ms', () => {
16 pm.expect(pm.response.responseTime).to.be.below(500);
17});
18
19pm.test('Content-Type is application/json', () => {
20 pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json');
21});
22
23// Extract token for subsequent requests
24const body = pm.response.json();
25if (body.accessToken) {
26 pm.environment.set('access_token', body.accessToken);
27 pm.collectionVariables.set('access_token', body.accessToken);
28}
29
30// GET /courses — Courses List Request
31pm.test('Status is 200', () => pm.expect(pm.response.code).to.eql(200));
32
33pm.test('Response has pagination structure', () => {
34 const body = pm.response.json();
35 pm.expect(body).to.have.property('data').that.is.an('array');
36 pm.expect(body).to.have.property('meta');
37 pm.expect(body.meta).to.have.property('total').that.is.a('number');
38 pm.expect(body.meta).to.have.property('page').that.is.a('number');
39});
40
41pm.test('Each course has required fields', () => {
42 const body = pm.response.json();
43 body.data.forEach(course => {
44 pm.expect(course).to.have.property('id');
45 pm.expect(course).to.have.property('title');
46 pm.expect(course).to.have.property('price').that.is.a('number');
47 });
48});1// Test a full enrollment workflow:
2// 1. Login → save token
3// 2. List courses → save first course ID
4// 3. Enroll in course → verify enrollment created
5// 4. Get progress → verify enrollment appears
6
7// Request 1: POST /auth/login
8// Tests (pre-request script extracts token):
9const body = pm.response.json();
10pm.environment.set('access_token', body.accessToken);
11
12// Request 2: GET /courses?limit=1
13pm.test('Got a course to enroll in', () => {
14 const body = pm.response.json();
15 pm.expect(body.data.length).to.be.greaterThan(0);
16});
17const firstCourseId = pm.response.json().data[0]?.id;
18pm.environment.set('test_course_id', firstCourseId);
19
20// Request 3: POST /progress/enroll
21// Body: { "courseId": "{{test_course_id}}" }
22// Authorization: Bearer {{access_token}}
23pm.test('Enrolled successfully (201 or 200)', () => {
24 pm.expect([200, 201]).to.include(pm.response.code);
25});
26const enrollment = pm.response.json();
27pm.environment.set('enrollment_id', enrollment.id);
28
29// Request 4: GET /progress (user's enrollments)
30pm.test('Progress includes new enrollment', () => {
31 const body = pm.response.json();
32 const enrollmentId = pm.environment.get('enrollment_id');
33 const found = body.data?.some(e => e.id === enrollmentId);
34 pm.expect(found).to.be.true;
35});1# Install Newman
2npm install -g newman
3npm install -g newman-reporter-junitfull # JUnit output for CI
4
5# Export collection from Postman:
6# Postman → Collection → ... → Export → Collection v2.1
7# Save as: tests/postman/api-collection.json
8
9# Export environment:
10# Postman → Environments → ... → Export
11# Save as: tests/postman/staging.environment.json
12
13# Run locally:
14newman run tests/postman/api-collection.json \
15 --environment tests/postman/staging.environment.json \
16 --delay-request 100 # 100ms between requests
17
18# Run in CI with JUnit XML report:
19newman run tests/postman/api-collection.json \
20 --environment tests/postman/ci.environment.json \
21 --reporters cli,junitfull \
22 --reporter-junitfull-export newman-results.xml \
23 --bail # stop on first failure1# .github/workflows/api-tests.yml
2- name: Run Newman API tests
3 run: |
4 newman run tests/postman/api-collection.json \
5 --environment tests/postman/staging.environment.json \
6 --reporters cli,junitfull \
7 --reporter-junitfull-export newman-results.xml
8
9- name: Publish test results
10 uses: mikepenz/action-junit-report@v4
11 if: always()
12 with:
13 report_paths: newman-results.xml
14 check_name: Newman API Tests1// Newman data files enable running the same request with multiple data sets
2// tests/postman/test-users.json:
3[
4 { "email": "[email protected]", "password": "Pass123!", "expectedRole": "user" },
5 { "email": "[email protected]", "password": "AdminPass123!", "expectedRole": "admin" },
6 { "email": "[email protected]", "password": "WrongPass", "expectedStatus": 401 }
7]
8
9// In Postman request, use {{email}} and {{password}} from data file
10// In Tests tab:
11
12const expectedStatus = pm.iterationData.get('expectedStatus') || 200;
13pm.test('Status matches expected', () => {
14 pm.expect(pm.response.code).to.eql(expectedStatus);
15});
16
17if (pm.response.code === 200) {
18 const expectedRole = pm.iterationData.get('expectedRole');
19 pm.test('User has correct role', () => {
20 const body = pm.response.json();
21 pm.expect(body.user.role).to.eql(expectedRole);
22 });
23}
24
25// Run with data file:
26// newman run collection.json \
27// --iteration-data tests/postman/test-users.jsonA Postman collection JSON with test scripts, environment variable extraction, and request chaining for a multi-step API workflow.
1{
2 "info": {
3 "name": "DeveloperBootstrap API Tests",
4 "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
5 },
6 "item": [
7 {
8 "name": "Auth",
9 "item": [
10 {
11 "name": "POST Login",
12 "event": [
13 {
14 "listen": "test",
15 "script": {
16 "exec": [
17 "pm.test('Status 200', () => pm.expect(pm.response.code).to.eql(200));",
18 "pm.test('Has accessToken', () => {",
19 " const body = pm.response.json();",
20 " pm.expect(body.accessToken).to.be.a('string');",
21 " pm.environment.set('access_token', body.accessToken);",
22 "});",
23 "pm.test('Response under 500ms', () => {",
24 " pm.expect(pm.response.responseTime).to.be.below(500);",
25 "});"
26 ]
27 }
28 }
29 ],
30 "request": {
31 "method": "POST",
32 "header": [{ "key": "Content-Type", "value": "application/json" }],
33 "body": {
34 "mode": "raw",
35 "raw": "{\n \"email\": \"{{admin_email}}\",\n \"password\": \"{{admin_password}}\"\n}"
36 },
37 "url": "{{base_url}}/auth/login"
38 }
39 }
40 ]
41 },
42 {
43 "name": "Courses",
44 "item": [
45 {
46 "name": "GET List Courses",
47 "event": [
48 {
49 "listen": "test",
50 "script": {
51 "exec": [
52 "pm.test('Status 200', () => pm.expect(pm.response.code).to.eql(200));",
53 "pm.test('Has pagination', () => {",
54 " const body = pm.response.json();",
55 " pm.expect(body.data).to.be.an('array');",
56 " pm.expect(body.meta.total).to.be.a('number');",
57 " if (body.data.length > 0) {",
58 " pm.environment.set('test_course_id', body.data[0].id);",
59 " }",
60 "});"
61 ]
62 }
63 }
64 ],
65 "request": {
66 "method": "GET",
67 "header": [{ "key": "Authorization", "value": "Bearer {{access_token}}" }],
68 "url": "{{base_url}}/courses?limit=10&page=1"
69 }
70 },
71 {
72 "name": "GET Course by ID",
73 "event": [
74 {
75 "listen": "test",
76 "script": {
77 "exec": [
78 "pm.test('Status 200', () => pm.expect(pm.response.code).to.eql(200));",
79 "pm.test('Has instructor', () => {",
80 " const body = pm.response.json();",
81 " pm.expect(body.instructor).to.exist;",
82 " pm.expect(body.instructor.passwordHash).to.not.exist;",
83 "});"
84 ]
85 }
86 }
87 ],
88 "request": {
89 "method": "GET",
90 "url": "{{base_url}}/courses/{{test_course_id}}"
91 }
92 }
93 ]
94 }
95 ]
96}This Postman collection demonstrates the standard patterns: environment variable extraction from responses (setting access_token after login, test_course_id from the course list), pm.test() for named assertions, and request chaining where the course list test populates the variable used by the 'GET Course by ID' request. Export this collection from Postman and run with: newman run collection.json --environment local.environment.json --reporters cli,junitfull
The next lesson covers request/response schema validation with Zod and Joi — verifying that API responses match the expected schema and that client requests are validated against strict type definitions.
Your progress is saved in this browser, on this device only.