Postman API Testing Tutorial for QA Engineers
Introduction to API Testing
Modern applications are built on APIs (Application Programming Interfaces). The frontend sends requests to the backend API, which processes them and returns responses. As a QA engineer, testing APIs is just as important as testing the UI — in fact, many argue it's even more critical because APIs handle the core business logic and data integrity.
Postman is the most popular tool for API testing. It provides a user-friendly interface for sending HTTP requests to APIs, inspecting responses, creating automated test scripts, and building comprehensive test suites. Whether you're testing a public REST API or your company's internal microservices, Postman is an essential skill for modern QA engineers.
Why API Testing Matters
Many bugs and security vulnerabilities exist only at the API layer. A UI test might pass, but the API could be returning sensitive data, validating incorrectly, or crashing under load. API testing allows you to:
- Test independently of the UI: Don't wait for frontend developers to finish the UI. Test the API as soon as it's available.
- Catch security issues: Test authorization, authentication, and input validation at the API level.
- Performance test at scale: Send thousands of concurrent requests to find bottlenecks.
- Test error scenarios: Trigger edge cases and errors that are hard to reproduce through the UI.
- Validate data integrity: Ensure data is saved correctly in the backend database.
What to Test in APIs
HTTP Status Code Reference
| Code | Meaning | When to Expect |
|---|---|---|
200 | OK | Successful GET / PUT |
201 | Created | Successful POST that created a resource |
204 | No Content | Successful DELETE |
400 | Bad Request | Invalid input / validation error |
401 | Unauthorized | Missing or invalid auth token |
403 | Forbidden | Authenticated but not authorized |
404 | Not Found | Resource doesn't exist |
422 | Unprocessable Entity | Semantic validation failed |
500 | Internal Server Error | Backend crash / unhandled exception |
Top Interview Questions — API Testing
What is the difference between GET and POST?
GET retrieves data — idempotent, no body, params in URL. POST creates data — has a request body, not idempotent (calling it twice may create two records).
What is REST?
REST (Representational State Transfer) is an architectural style for APIs using standard HTTP methods (GET, POST, PUT, DELETE) and stateless communication.
How do you test authentication?
Test positive cases (valid token → 200), negative cases (no token → 401, expired → 401), and authorization (valid token wrong role → 403).
What is idempotency?
An operation is idempotent if calling it multiple times produces the same result. GET, PUT, and DELETE are idempotent. POST is not.
HTTP Method Reference
| Method | Action | Has Body? | QA Test Case |
|---|---|---|---|
GET | Retrieve resource | No | Fetch user list, get product by ID |
POST | Create resource | Yes | Register user, create order |
PUT | Replace entire resource | Yes | Replace full user record |
PATCH | Partial update | Yes | Update only the email field |
DELETE | Remove resource | No | Delete a test account |
HEAD | GET but no body returned | No | Check if resource exists |
OPTIONS | Check allowed methods | No | CORS preflight check |
Sample Requests in Postman
GET {{baseUrl}}/api/users GET {{baseUrl}}/api/users/{{userId}} POST {{baseUrl}}/api/users Body (JSON): { "name": "QA Tester", "email": "test@example.com", "role": "tester" } DELETE {{baseUrl}}/api/users/{{userId}}
// Content-Type — tells server what format body is in Content-Type: application/json // Authorization — pass authentication tokens Authorization: Bearer eyJhbGciOiJIUzI1NiIs... // Custom headers X-API-Key: your-api-key-here X-Request-ID: unique-request-id // Accept — tells server what format you want in response Accept: application/json // Cache control Cache-Control: no-cache, no-store If-None-Match: "abc123" // ETag for conditional requests // CORS headers Origin: https://example.com Access-Control-Request-Method: POST
// Server tells you response format Content-Type: application/json; charset=utf-8 // Cache instructions Cache-Control: max-age=3600, public // CORS permissions Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, PUT, DELETE // Response timestamp and expiry Date: Mon, 16 Apr 2026 10:30:00 GMT Expires: Mon, 16 Apr 2026 11:30:00 GMT // Rate limiting X-RateLimit-Limit: 100 X-RateLimit-Remaining: 99 X-RateLimit-Reset: 1682774400 // Content encoding Content-Encoding: gzip
REQUEST LINE: POST /api/users HTTP/1.1 HEADERS: Host: api.example.com Content-Type: application/json Content-Length: 145 Authorization: Bearer token123 BODY (JSON): { "name": "John Doe", "email": "john@example.com", "age": 30, "active": true } QUERY PARAMS: {{baseUrl}}/api/users?page=1&limit=10&sort=name
STATUS LINE: HTTP/1.1 201 Created HEADERS: Content-Type: application/json Content-Length: 234 Server: nginx/1.21.0 Date: Mon, 16 Apr 2026 10:30:00 GMT X-Response-Time: 145ms BODY (JSON): { "id": 42, "name": "John Doe", "email": "john@example.com", "createdAt": "2026-04-16T10:30:00Z", "status": "active" }
// Bearer Token (JWT) Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... // API Key in header X-API-Key: sk_live_abc123xyz456 // API Key in query parameter {{baseUrl}}/api/users?api_key=sk_live_abc123xyz456 // Basic Authentication (Base64 encoded) Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= // OAuth 2.0 Bearer token Authorization: Bearer access_token_xyz123 // Custom header Authorization: CustomScheme abc123xyz456
// Generate JWT token in pre-request const secret = 'your-secret-key' const payload = { sub: 'user123', iat: Math.floor(Date.now() / 1000) } // Note: Full JWT generation requires crypto library or external service // For testing, get token from login endpoint // Simulate getting token from login response pm.environment.set('authToken', 'token_from_login_response')
// Test 1: Valid token → 200 OK pm.test("Valid token returns 200", () => { pm.response.to.have.status(200) }) // Test 2: No token → 401 Unauthorized pm.test("Missing token returns 401", () => { pm.response.to.have.status(401) }) // Test 3: Expired token → 401 pm.test("Expired token returns 401", () => { pm.response.to.have.status(401) }) // Test 4: Valid token, wrong permissions → 403 Forbidden pm.test("Insufficient permissions returns 403", () => { pm.response.to.have.status(403) })
Collections & Environments
{{variableName}} syntax.Environment Variables Usage
// In URL {{baseUrl}}/api/users/{{userId}} // In Headers Authorization: Bearer {{authToken}} // In Body { "email": "{{testEmail}}", "role": "{{testRole}}" } // Dev environment values baseUrl = https://dev.api.techworldlabs.com authToken = dev-token-abc123 // Staging environment values baseUrl = https://staging.api.techworldlabs.com authToken = staging-token-xyz456
// Status code checks pm.test("Status is 200", () => { pm.response.to.have.status(200) }) // Response time pm.test("Response time under 500ms", () => { pm.expect(pm.response.responseTime).to.be.below(500) }) // Header checks pm.test("Has JSON content type", () => { pm.response.to.have.header('Content-Type') pm.expect(pm.response.headers.get('Content-Type')).include('application/json') }) // Body property existence pm.test("Response has user ID", () => { const data = pm.response.json() pm.expect(data).to.have.property('id') }) // Exact value match pm.test("User role is admin", () => { const data = pm.response.json() pm.expect(data.role).to.eq('admin') }) // Array length pm.test("User has 3 roles", () => { const data = pm.response.json() pm.expect(data.roles).to.have.length(3) }) // Array contains value pm.test("Roles include editor", () => { const data = pm.response.json() pm.expect(data.roles).to.include('editor') })
Script Tabs
Pre-request Script
Runs before the request is sent. Used to generate dynamic data, set variables, or set auth tokens programmatically.
Tests Tab
Runs after the response is received. Used to assert status codes, response time, body fields, and save values to environment variables.
pm object
Postman's built-in JavaScript object. Access pm.response, pm.environment, pm.test(), pm.expect() and more.
Pre-request & Test Script Examples
// Set a dynamic timestamp variable pm.environment.set('timestamp', Date.now()) // Generate unique test email pm.environment.set('testEmail', `qa+${Date.now()}@test.com`) // Generate random user ID const randomId = Math.floor(Math.random() * 10000) pm.environment.set('randomUserId', randomId)
// Status code check pm.test("Status is 200", () => { pm.response.to.have.status(200) }) // Response time SLA pm.test("Response under 500ms", () => { pm.expect(pm.response.responseTime).to.be.below(500) }) // JSON body assertions pm.test("User has required fields", () => { const data = pm.response.json() pm.expect(data).to.have.property("id") pm.expect(data).to.have.property("email") pm.expect(data.role).to.eq("admin") }) // Save response value to environment const res = pm.response.json() pm.environment.set("userId", res.id) pm.environment.set("authToken", res.token)
How do you chain API calls in Postman?
In the Tests tab of request 1, save a response value: pm.environment.set("token", res.token). Then use {{token}} in the Authorization header of request 2.
What is pm.expect()?
pm.expect() uses Chai.js BDD assertions inside pm.test(). It lets you write readable assertions like pm.expect(data.status).to.eq("active").
[
{
"name": "User One",
"email": "user1@example.com",
"role": "admin",
"expectedStatus": 201
},
{
"name": "User Two",
"email": "user2@example.com",
"role": "user",
"expectedStatus": 201
},
{
"name": "",
"email": "invalid",
"role": "user",
"expectedStatus": 400
}
]
// In Tests tab — validate using data from CSV/JSON pm.test("Status matches expected value", () => { const expectedStatus = pm.iterationData.get("expectedStatus") pm.response.to.have.status(expectedStatus) }) // Validate response data matches input pm.test("Response email matches request", () => { const inputEmail = pm.iterationData.get("email") const responseEmail = pm.response.json().email pm.expect(responseEmail).to.eq(inputEmail) })
// Request 1: Login (Tests tab) const res = pm.response.json() pm.environment.set('authToken', res.token) pm.environment.set('userId', res.userId) // Request 2: Create User (Uses token from login) URL: {{baseUrl}}/api/users Header: Authorization: Bearer {{authToken}} Body: { "name": "Test User" } // Request 2 Tests tab (save user ID) const newUser = pm.response.json() pm.environment.set('newUserId', newUser.id) // Request 3: Get User (Uses newUserId from create) URL: {{baseUrl}}/api/users/{{newUserId}} Header: Authorization: Bearer {{authToken}} // Request 4: Update User (Uses newUserId) URL: {{baseUrl}}/api/users/{{newUserId}} Method: PATCH Body: { "name": "Updated Name" } // Request 5: Delete User (Uses newUserId) URL: {{baseUrl}}/api/users/{{newUserId}} Method: DELETE
// Basic response time check pm.test("Response time under 500ms", () => { pm.expect(pm.response.responseTime).to.be.below(500) }) // Response time by status pm.test("GET under 200ms, POST under 500ms", () => { const method = pm.request.method if (method === 'GET') { pm.expect(pm.response.responseTime).to.be.below(200) } else if (method === 'POST') { pm.expect(pm.response.responseTime).to.be.below(500) } }) // Average response time (from multiple requests) // Can be tracked via Newman in CI/CD reports
// Test 1: No auth token provided pm.test("Unauthenticated request returns 401", () => { pm.response.to.have.status(401) }) // Test 2: Invalid token format pm.test("Invalid token returns 401", () => { pm.response.to.have.status(401) }) // Test 3: User cannot access other user data pm.test("Cannot access other user (403)", () => { // Login as user A, try to access user B data pm.response.to.have.status(403) }) // Test 4: SQL injection attempt pm.test("SQL injection attempt fails safely", () => { // URL: /api/users?id=1' OR '1'='1 const data = pm.response.json() pm.expect(data.error).to.include('Invalid') // Should not return all users }) // Test 5: No sensitive data in response pm.test("Response doesn't leak passwords", () => { const data = pm.response.json() pm.expect(JSON.stringify(data)).to.not.include('password') }) // Test 6: CORS headers present pm.test("CORS headers configured", () => { pm.response.to.have.header('Access-Control-Allow-Origin') })
Setting Up a Mock Server
Create Examples
In Postman, save example responses for each request — name them "200 Success", "404 Not Found", "500 Error" etc.
Create Mock Server
In your Collection, click "…" → Mock Collection. Postman generates a unique public mock URL instantly.
Use in Tests
Point your environment's baseUrl to the mock server URL. Requests will receive your saved example responses.
Test Edge Cases
Create examples for error states (500, 404, 403) to test how your app handles failures without needing real backend errors.
baseUrl in your environment file — zero test changes needed.What is a mock server in API testing?
A mock server simulates backend API responses using predefined examples. It lets QA teams test the frontend/integration layer before the real backend is available.
Mock vs Stub vs Fake — what is the difference?
A mock verifies interactions (was this endpoint called?). A stub returns predefined responses. A fake is a lightweight working implementation. In everyday QA talk, "mock server" usually means stub behaviour.
When would you use a mock server?
When the backend isn't ready, when you want deterministic responses for edge cases (e.g. simulate a 500 error), or when you want tests to run without hitting a real external service.
// Collection structure for maintainability 📦 API Tests Collection ├── 📁 Authentication │ ├── Login │ ├── Logout │ └── Refresh Token ├── 📁 Users │ ├── List Users │ ├── Get User │ ├── Create User │ ├── Update User │ └── Delete User ├── 📁 Products │ ├── List Products │ ├── Get Product │ └── Search Products └── 📁 Orders ├── Create Order ├── Get Order └── Cancel Order // Use Collection and Environment files for CI/CD postman_collection.json (all requests) dev.json (dev environment variables) staging.json (staging environment variables) prod.json (prod environment variables)
// Collection-level Pre-request script (setup) Runs before EVERY request in collection: pm.environment.set('testRunId', pm.globals.get('testRunId') || Date.now()) // Collection-level Tests script (teardown) Runs after EVERY response in collection: pm.test("Response is valid", () => { pm.response.to.not.be.error }) // Skip/abort tests conditionally const skipTest = pm.environment.get('skipThisFolder') if (skipTest) { pm.test.skip("Test skipped") }
name: API Tests (Postman)
on:
push:
branches: [main, develop]
schedule:
# Run nightly at 2 AM
- cron: '0 2 * * *'
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install Newman
run: npm install -g newman newman-reporter-htmlextra
- name: Run API Tests (Dev)
run: |
newman run postman_collection.json \
-e dev.json \
--reporters cli,htmlextra \
--reporter-htmlextra-export reports/api-test-report.html
- name: Upload Report
if: always()
uses: actions/upload-artifact@v3
with:
name: api-test-reports
path: reports/
// ═══ GET REQUEST (Fetch User) ═══ // URL: {{baseUrl}}/api/users/{{userId}} // PRE-REQUEST SCRIPT // Validate userId exists const userId = pm.environment.get('userId') if (!userId) { throw new Error('userId not set in environment') } console.log('GET request for userId:', userId) // TEST SCRIPT pm.test("GET: Status 200 OK", () => { pm.response.to.have.status(200) }) pm.test("GET: Response time under 300ms", () => { pm.expect(pm.response.responseTime).to.be.below(300) }) pm.test("GET: Response has required fields", () => { const data = pm.response.json() pm.expect(data).to.have.property('id') pm.expect(data).to.have.property('name') pm.expect(data).to.have.property('email') }) pm.test("GET: ID matches request", () => { const data = pm.response.json() pm.expect(data.id).to.eq(pm.environment.get('userId')) }) // Save data for next request const userData = pm.response.json() pm.environment.set('userName', userData.name) pm.environment.set('userEmail', userData.email)
// ═══ POST REQUEST (Create User) ═══ // URL: {{baseUrl}}/api/users // Body: raw JSON // PRE-REQUEST SCRIPT const timestamp = Date.now() const newUser = { "name": `QA User ${timestamp}`, "email": `qa+${timestamp}@test.com`, "role": "tester", "active": true } // Set request body pm.request.body.raw = JSON.stringify(newUser) // Store for validation later pm.environment.set('postPayload', JSON.stringify(newUser)) console.log('POST payload:', newUser) // TEST SCRIPT pm.test("POST: Status 201 Created", () => { pm.response.to.have.status(201) }) pm.test("POST: Response contains ID", () => { const data = pm.response.json() pm.expect(data).to.have.property('id') pm.expect(data.id).to.be.above(0) }) pm.test("POST: Response matches request data", () => { const response = pm.response.json() const payload = JSON.parse(pm.environment.get('postPayload')) pm.expect(response.name).to.eq(payload.name) pm.expect(response.email).to.eq(payload.email) pm.expect(response.role).to.eq(payload.role) }) // IMPORTANT: Save created ID for subsequent requests const createdUser = pm.response.json() pm.environment.set('createdUserId', createdUser.id) pm.environment.set('createdUserEmail', createdUser.email) console.log('Created user ID:', createdUser.id)
// ═══ PUT REQUEST (Update User - Replace) ═══ // URL: {{baseUrl}}/api/users/{{createdUserId}} // Body: raw JSON (complete object required) // PRE-REQUEST SCRIPT const updatedUser = { "name": "Updated QA User", "email": pm.environment.get('createdUserEmail'), "role": "senior-tester", "active": true, "updatedAt": new Date().toISOString() } pm.request.body.raw = JSON.stringify(updatedUser) pm.environment.set('putPayload', JSON.stringify(updatedUser)) console.log('PUT payload:', updatedUser) // TEST SCRIPT pm.test("PUT: Status 200 OK", () => { pm.response.to.have.status(200) }) pm.test("PUT: Name updated correctly", () => { const response = pm.response.json() pm.expect(response.name).to.eq("Updated QA User") }) pm.test("PUT: Role updated to senior-tester", () => { const response = pm.response.json() pm.expect(response.role).to.eq("senior-tester") }) pm.test("PUT: Response time under 400ms", () => { pm.expect(pm.response.responseTime).to.be.below(400) }) pm.test("PUT: ID unchanged", () => { const response = pm.response.json() pm.expect(response.id).to.eq(pm.environment.get('createdUserId')) }) console.log('User updated successfully')
// ═══ PATCH REQUEST (Partial Update) ═══ // URL: {{baseUrl}}/api/users/{{createdUserId}} // Body: raw JSON (only fields to update) // PRE-REQUEST SCRIPT const partialUpdate = { "email": `qa+updated+${Date.now()}@test.com` // Note: only updating email, other fields unchanged } pm.request.body.raw = JSON.stringify(partialUpdate) console.log('PATCH payload (partial):', partialUpdate) // TEST SCRIPT pm.test("PATCH: Status 200 OK", () => { pm.response.to.have.status(200) }) pm.test("PATCH: Email updated", () => { const response = pm.response.json() const payload = JSON.parse(pm.request.body.raw) pm.expect(response.email).to.eq(payload.email) }) pm.test("PATCH: Other fields unchanged", () => { const response = pm.response.json() pm.expect(response.name).to.eq("Updated QA User") pm.expect(response.role).to.eq("senior-tester") }) // Update environment with new email pm.environment.set('createdUserEmail', pm.response.json().email)
// ═══ DELETE REQUEST (Delete User) ═══ // URL: {{baseUrl}}/api/users/{{createdUserId}} // No Body needed for DELETE // PRE-REQUEST SCRIPT const userIdToDelete = pm.environment.get('createdUserId') if (!userIdToDelete) { throw new Error('No user ID to delete') } console.log('Deleting user:', userIdToDelete) // TEST SCRIPT pm.test("DELETE: Status 200 or 204", () => { pm.expect([200, 204]).to.include(pm.response.code) }) pm.test("DELETE: Response time under 300ms", () => { pm.expect(pm.response.responseTime).to.be.below(300) }) // If 200 response with message pm.test("DELETE: Confirms deletion", () => { if (pm.response.code === 200) { const data = pm.response.json() pm.expect(data.message || data.success).to.be.ok } }) // Clear environment variables after delete pm.environment.unset('createdUserId') pm.environment.unset('createdUserEmail') console.log('User deleted and environment cleared')
// ═══ COLLECTION-LEVEL PRE-REQUEST (Runs before EVERY request) ═══ // Set baseUrl if not exists if (!pm.environment.get('baseUrl')) { pm.environment.set('baseUrl', 'https://api.techworldlabs.com') } // Log every request console.log(` [${new Date().toISOString()}] ${pm.request.method} ${pm.request.url.toString()} `) // ═══ COLLECTION-LEVEL TESTS (Runs after EVERY response) ═══ pm.test("Response is valid HTTP", () => { pm.response.to.not.be.error }) pm.test("Response has Content-Type", () => { pm.response.to.have.header('Content-Type') }) pm.test("No timeout occurred", () => { pm.expect(pm.response.code).to.not.eq(0) }) // ═══ COMPLETE WORKFLOW TEST EXECUTION ═══ // 1. GET /api/users (list all users) - Pass // 2. POST /api/users (create new user) - Save ID - Pass // 3. GET /api/users/{id} (fetch created user) - Pass // 4. PUT /api/users/{id} (update user) - Pass // 5. PATCH /api/users/{id} (partial update) - Pass // 6. DELETE /api/users/{id} (delete user) - Pass
# Run with retries on failure newman run collection.json \ -e dev.json \ --delay-request 500 \ --timeout-request 5000 # Run specific folder only newman run collection.json --folder "Authentication" # Run multiple times (load test) newman run collection.json -n 10 # Use different reporters newman run collection.json \ --reporters cli,json,htmlextra \ --reporter-json-export results.json \ --reporter-htmlextra-export report.html # Export summary to file newman run collection.json > test-run.log 2&1
Why Newman?
--folder.Newman Commands & GitHub Actions
# Install Newman globally npm install -g newman # Install HTML reporter npm install -g newman-reporter-htmlextra # Run collection with environment newman run TechWorldLabs.postman_collection.json \ -e Dev.postman_environment.json # Run with HTML report newman run TechWorldLabs.postman_collection.json \ -e Dev.postman_environment.json \ --reporters htmlextra \ --reporter-htmlextra-export reports/api-report.html # Run specific folder only newman run collection.json --folder "User API Tests"
name: API Tests on: [push] jobs: api-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install Newman run: npm install -g newman - name: Run API Tests run: newman run collection.json -e staging.json
// Log to Postman console console.log('Request URL:', pm.request.url) console.log('Headers:', pm.request.headers) console.log('Response status:', pm.response.code) // Print full response body console.log('Response body:', pm.response.text()) // Inspect JSON response const jsonData = pm.response.json() console.log('JSON data:', JSON.stringify(jsonData, null, 2)) // Check specific field console.log('User ID:', jsonData.id) console.log('Auth token:', pm.environment.get('authToken')) // Debug test failures if (pm.response.code !== 200) { console.error('Test failed! Status:', pm.response.code) console.error('Response:', pm.response.text()) }
// Tools available in Postman: 1. Console (View → Show Postman Console) - See all request/response logs - View console.log() output - Filter by request name 2. Network tab - Inspect request/response details - See headers, body, timings - Copy as curl command 3. Collection Runner - Run requests sequentially - Easily spot which request fails - See iteration data 4. Postman Interceptor Chrome extension - Capture live traffic from your browser - Inspect and replay requests - Test without manually sending
Best Practices
Common Issues
CORS errors in tests
Cause: Cross-origin requests blocked. Solution: Postman ignores CORS. Use proxy or API server that allows cross-origin.
Authentication timeouts
Cause: Token expired. Solution: Refresh tokens in Pre-request Script or use OAuth flows.
Tests pass locally but fail in CI
Cause: Environment differences. Solution: Use same variables in CI environment as local.
Response timeout issues
Cause: Slow API or network. Solution: Increase request timeout in settings or optimize API performance.
FAQs
How do I extract values from responses for next request?
Use pm.environment.set() or pm.variables.set() in Tests tab to extract and store response values.
How do I handle file uploads in Postman?
Select 'form-data', add key, select 'File' type, choose file. Postman handles multipart/form-data encoding.
Can I run requests in sequence?
Yes, use pm.setNextRequest('Request Name') in Tests tab to control flow.
How do I test XML APIs?
Use application/xml content type. Parse XML responses using pm.response.xml() for assertions.
How do I test WebSockets?
Postman doesn't support WebSockets natively. Use tools like wscat or Cypress for WebSocket testing.
How do I generate test reports?
Run with Newman, use --reporters cli,json to generate reports. Integrate with Newman reporters for HTML output.