When I started API testing, I spent the first week confused about why I needed to test something that had no user interface. The frontend looked fine. Why would I poke at the backend directly? Then a production bug slipped through because the API was returning a 200 status code with an empty response body, and the frontend was silently displaying nothing. The UI "worked." The data was missing. Nobody caught it until a customer complained. That is when API testing clicked for me. You are testing the contract between the frontend and the backend. If that contract breaks, everything downstream breaks.
Setting Up Your First Request
Open Postman, create a new collection called "My API Tests," and send your first request.
GET request:
Method: GET
URL: https://jsonplaceholder.typicode.com/posts/1Hit Send. You get:
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident...",
"body": "quia et suscipit..."
}Status: 200 OK. That is your first API test. You confirmed the endpoint exists, responds, and returns data in the expected format.
Adding Assertions (Tests Tab)
A request without assertions is not a test. It is just a call. Click the Tests tab in Postman and add:
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
pm.test("Response has required fields", function () {
const json = pm.response.json();
pm.expect(json).to.have.property("id");
pm.expect(json).to.have.property("title");
pm.expect(json).to.have.property("body");
pm.expect(json).to.have.property("userId");
});
pm.test("ID matches requested resource", function () {
const json = pm.response.json();
pm.expect(json.id).to.eql(1);
});
pm.test("Response time is under 500ms", function () {
pm.expect(pm.response.responseTime).to.be.below(500);
});Now when you hit Send, Postman runs these assertions and tells you if they pass or fail. This is a real test.
Environment Variables
Hardcoding URLs is a mistake you make once. When you need to test against staging, production, and local environments, variables save you. Create an environment in Postman:
Variable: base_url
Development: http://localhost:3000/api
Staging: https://staging.example.com/api
Production: https://api.example.comNow your request URL becomes:
GET {{base_url}}/posts/1Switch environments with one click. Same tests, different servers.
Testing a POST Request
Creating resources is where API testing gets interesting. You are validating that the server accepts data, processes it correctly, and returns the right response.
Method: POST
URL: {{base_url}}/posts
Headers:
Content-Type: application/json
Body (raw JSON):
{
"title": "Test Post",
"body": "This is a test post body",
"userId": 1
}Assertions for POST:
pm.test("Status code is 201 Created", function () {
pm.response.to.have.status(201);
});
pm.test("Response contains the created resource", function () {
const json = pm.response.json();
pm.expect(json.title).to.eql("Test Post");
pm.expect(json).to.have.property("id");
});
// Store the created ID for use in subsequent requests
pm.test("Store created ID", function () {
const json = pm.response.json();
pm.environment.set("created_post_id", json.id);
});That last assertion stores the created resource ID so you can use it in follow-up requests (GET, PUT, DELETE) without hardcoding.
Chaining Requests
Real API testing is not individual requests. It is workflows. Create a post, retrieve it, update it, delete it, verify it is gone. Collection order:
- POST /posts (create) ➜ stores
created_post_id - GET /posts/ (verify creation)
- PUT /posts/ (update)
- GET /posts/ (verify update)
- DELETE /posts/ (delete)
- GET /posts/ (verify deletion returns 404)
Each request uses the variable from the previous one. Run the entire collection with Postman's Collection Runner and all six requests execute in sequence.
Testing Error Responses
This is where most QA engineers stop too early. Testing that the API works is step one. Testing that it fails correctly is step two.
Test: POST /posts with missing required field
Body: { "body": "No title provided" }
Expected: 400 Bad Request with error message
Test: GET /posts/999999
Expected: 404 Not Found
Test: POST /posts with invalid JSON
Body: { invalid json }
Expected: 400 Bad Request
Test: DELETE /posts/1 without auth token
Expected: 401 UnauthorizedAssertions for error responses:
pm.test("Returns 400 for missing required field", function () {
pm.response.to.have.status(400);
});
pm.test("Error response has message", function () {
const json = pm.response.json();
pm.expect(json).to.have.property("error");
pm.expect(json.error).to.be.a("string");
});Authentication Testing
Most real APIs require authentication. Test the auth flow thoroughly:
// Login request: store the token
pm.test("Login returns token", function () {
const json = pm.response.json();
pm.expect(json).to.have.property("token");
pm.environment.set("auth_token", json.token);
});
// Subsequent requests: use the token
// In the Headers tab:
// Authorization: Bearer {{auth_token}}Auth edge cases to test:
- Expired token: does the API return 401?
- Malformed token: does it fail gracefully?
- Missing token: does it return 401, not 500?
- Token from a different user: does authorization work?
Running Collections in CI/CD
Postman tests are not just for manual runs. Export your collection and use Newman (Postman's CLI runner) in your CI/CD pipeline:
# Install Newman
npm install -g newman
# Run a collection
newman run my-api-tests.json \
--environment staging.json \
--reporters cli,junit \
--reporter-junit-export results.xmlThis runs your entire API test collection on every deploy. If any assertion fails, the pipeline fails.
My API Testing Checklist
Every API endpoint I test goes through this:
Functional
- Correct status code for success
- Response body has all required fields
- Data types are correct (string, number, array)
- Pagination works (if applicable)
- Filtering and sorting return correct results
Error handling
- Missing required fields return 400
- Invalid data types return 400
- Non-existent resources return 404
- Unauthorized access returns 401
- Forbidden access returns 403
Performance
- Response time under acceptable threshold
- Large payloads do not timeout
- Concurrent requests handled correctly
Security
- Authentication required on protected endpoints
- Authorization enforced (user A cannot access user B data)
- Input validation prevents injection
- Rate limiting active
Start with the functional checks, then work through errors, performance, and security. Over time this becomes second nature, and you will catch issues that never make it to production.