QA How-To
API Testing with Postman: A Step-by-Step Tutorial
Learn API testing in Postman by building requests, managing environments, validating JSON, chaining data, testing errors, and running collections in CI.
1,976 words | Article schema | FAQ schema | Breadcrumb schema
Overview
Postman is most useful when it becomes more than a place to click Send. A strong collection documents the contract, creates its own data, validates meaningful behavior, and can run without a person at the keyboard. That makes it valuable to manual testers learning APIs, developers checking a service, and automation engineers building a fast regression layer below the browser.
This tutorial builds a collection around a fictional bookstore REST API. You will inspect a health endpoint, authenticate, create a book, retrieve it, test invalid input, and remove the record. Along the way you will use environments, JavaScript assertions, variables, JSON schemas, and the collection runner. The exact URLs are examples, but the workflow is designed so you can replace the base URL and field names with those of a real API.
Learn the Request and Response Contract
An HTTP request has a method, URL, headers, optional query parameters, and sometimes a body. A response has a status code, headers, body, and timing. The method communicates intent: GET reads, POST creates or triggers, PUT replaces, PATCH partially updates, and DELETE removes. These are conventions rather than guarantees, so read the API specification and acceptance criteria before declaring behavior correct.
Do not reduce testing to status codes. For each endpoint, identify required fields, data types, value boundaries, authentication rules, authorization by role, idempotency expectations, error format, and persistent side effects. A 200 response containing the wrong customer's order is far more serious than a 500. Write these expectations near the request description so a collection remains understandable when its original author is unavailable.
Create a Collection and Environment
Create a collection named Bookstore API Tutorial, then create an environment with `baseUrl`, `username`, and `password`. Set `baseUrl` to a non-production test service such as `https://api.test.example.com`. Reference it in requests as `{{baseUrl}}/health`. Variables remove duplicated hostnames and let the same collection run against local, integration, or staging systems without editing every request. Give environments unmistakable names so a tester does not send destructive tutorial requests to production.
Understand scope before using variables. Global variables are visible broadly and can create accidental coupling. Collection variables suit values shared only by this suite. Environment variables represent a target deployment. Local variables exist during a script, and data variables come from runner input. Use the narrowest useful scope. Mark secret values as sensitive where supported, share placeholder keys rather than real credentials, and let CI inject secrets at runtime.
- Keep the base URL in an environment variable.
- Keep workflow IDs in collection variables when requests share them.
- Use local variables for temporary calculations inside one script.
- Never export real access tokens or passwords into source control.
Send and Validate a Health Request
Add a GET request named Health check with URL `{{baseUrl}}/health`. Click Send and inspect the status, response headers, body, and time. In the post-response script area, add `pm.test('status is 200', () => { pm.response.to.have.status(200); }); pm.test('service reports ready', () => { const body = pm.response.json(); pm.expect(body.status).to.eql('ready'); });`. Send again and confirm both tests pass. Deliberately change the expected status once to see how a failed assertion appears, then restore it.
Parsing with `pm.response.json()` makes the script work with JavaScript values instead of fragile text matching. Add a content-type check: `pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json')`. A response-time threshold can detect a severe regression, but choose it from service objectives and a stable test environment. Arbitrary limits such as 200 milliseconds often create noise caused by network location rather than application performance. Record the environment when sharing timing evidence.
Authenticate Without Exposing Secrets
Create a POST request to `{{baseUrl}}/auth/login`, choose raw JSON, and use environment variables for the username and password. Confirm the `Content-Type: application/json` header. Test successful authentication and the token shape. Then save it with `const json = pm.response.json(); pm.collectionVariables.set('accessToken', json.accessToken);`. Configure collection-level Bearer Token authorization using `{{accessToken}}` so child requests inherit it. Check that inherited authorization is not accidentally overridden on an individual request.
A collection should also test authentication failures deliberately: missing token, malformed token, and expired token when the environment supports controlled expiry. Separate authentication, which proves identity, from authorization, which decides what that identity may do. A standard user receiving 401 for an admin-only action may reveal an incorrect contract if the service uses 403 for authenticated but forbidden callers. Assert the documented behavior and confirm error bodies do not disclose internal stack traces or credential details.
Create a Resource and Capture Its ID
Add POST `{{baseUrl}}/books` with a body containing a title, author, and ISBN. A robust test checks 201, validates returned fields, verifies the identifier is present, and captures it: `pm.test('book is created', () => { pm.response.to.have.status(201); const book = pm.response.json(); pm.expect(book.title).to.eql('API Testing Field Notes'); pm.expect(book.id).to.be.a('string').and.not.empty; pm.collectionVariables.set('bookId', book.id); });`. Also confirm the response location header when the API contract promises one, because clients may use it to retrieve the new resource.
Use generated unique values if the API enforces uniqueness. In a pre-request script, `pm.collectionVariables.set('isbn', '978' + Date.now().toString().slice(-10));` can create a tutorial value, though a production framework should use a proper valid-data factory. Store only what downstream requests need. Capturing the complete response as many individual globals makes tests dependent on stale state and difficult to reset. Log the generated identifier when diagnosis requires it.
Verify Persistence With a Follow-Up GET
Add GET `{{baseUrl}}/books/{{bookId}}` after the create request. Assert 200 and compare title, author, and ISBN with the data that was submitted. This follow-up matters because a create endpoint can return an optimistic body even when persistence fails. It also verifies that the ID routes to the correct record and that serialization remains consistent across endpoints. If writes are eventually consistent, poll the documented status with a bounded timeout instead of inserting an arbitrary delay.
Test response structure without overfitting to every harmless field. A JSON Schema assertion can require core properties and types: `const schema = { type: 'object', required: ['id', 'title', 'author'], properties: { id: { type: 'string' }, title: { type: 'string' }, author: { type: 'string' } } }; pm.test('matches book contract', () => { pm.response.to.have.jsonSchema(schema); });`. Add `additionalProperties: false` only if the contract explicitly forbids new fields, since strict schemas can turn backward-compatible additions into false alarms.
Design Negative and Boundary Tests
Duplicate the create request into focused negative cases. Omit `title`, send an empty string, exceed the maximum length, use the wrong data type, submit malformed JSON, and try a duplicate ISBN. For each case, assert the specific documented status and a stable machine-readable error code. A useful check is `pm.expect(error).to.include.keys('code', 'message')` followed by an exact code assertion. Avoid asserting an entire human-readable message if wording can change without breaking clients.
Negative testing must also verify side effects. After a rejected creation, query by ISBN and confirm no book was stored. For updates, send a request as a user who owns a different record and prove both that access is denied and that the original data is unchanged. Include boundary values just below, at, and just above limits. A test that only sends nonsense inputs misses the off-by-one errors most likely to reach production.
- Missing required field and explicit null are separate cases.
- Test valid minimum and maximum lengths plus adjacent invalid values.
- Verify wrong types, malformed JSON, and unsupported media types.
- Exercise unauthenticated and unauthorized access separately.
- Confirm rejected requests do not partially persist data.
Clean Up and Make Runs Repeatable
Add DELETE `{{baseUrl}}/books/{{bookId}}` at the end and assert the documented success code, commonly 204. Follow it with GET and expect 404 to prove deletion. Cleanup requests should tolerate the record already being absent when an earlier assertion stops the intended flow. If the runner continues after failed requests, a final cleanup folder or service-side test-data expiration prevents abandoned records from polluting later runs.
Order-dependent workflows are acceptable when the sequence itself is the scenario, but make that dependency explicit in names and folder order. Independent endpoint tests should create their own prerequisites instead of relying on whatever a previous manual send left in the environment. Before sharing the collection, clear current variable values that contain tokens and reset resource IDs. A new teammate should be able to choose an environment and run the entire collection from a clean state.
Run the Collection and Move It Into CI
The Collection Runner executes requests in collection order and can repeat them with rows from CSV or JSON data files. Use data-driven runs for a small set of meaningful input partitions, not hundreds of redundant combinations. Inspect test totals, failed assertions, request timing, and console output. A passing run should mean all required cleanup and contract checks completed, not only that every server returned some response.
Export the collection and a secret-free environment to version control, then run them with Postman's command-line tooling or Newman according to the team's supported workflow. A typical Newman command is `newman run Bookstore.postman_collection.json -e Test.postman_environment.json --reporters cli,junit --reporter-junit-export results.xml`. Inject sensitive values through environment options or CI secrets, publish the JUnit report, and fail the pipeline on failed assertions. Pin the CLI version so local and build-agent behavior stays consistent.
Review the Collection Like Test Code
Give requests behavior-oriented names, describe preconditions, and keep assertions close to the endpoint contract. Extract genuinely shared checks, such as a standard error envelope, into collection-level scripts only when failures still show clear context. Log useful identifiers, not tokens or personal data. Review collection changes through source control so a relaxed assertion or deleted negative case receives the same scrutiny as application code.
Track whether failures come from the product, test script, shared environment, expired credentials, or unavailable dependencies. Flaky API tests frequently expose data collisions or unstable environments rather than timing in the test client. Use unique records and deterministic cleanup before adding retries. As the collection grows, divide it by capability, tag or folder critical smoke requests, and keep the suite fast enough to run before UI tests. API feedback is most valuable when developers receive it minutes after a contract changes.
Frequently Asked Questions
Can Postman be used for automated API testing?
Yes. JavaScript test scripts can assert each response, and collections can run from the Collection Runner or command-line tooling. CI can fail on assertion errors and publish reports without opening the Postman desktop application.
What should I validate in a Postman API test?
Validate the documented status, important headers, required body fields and types, business values, authorization rules, and persistent side effects. Include error behavior and confirm rejected operations do not modify data.
What is the difference between collection and environment variables?
Collection variables belong to one collection and suit workflow values such as a created record ID. Environment variables represent a target system, such as its base URL and test username. Use the narrowest scope that fits the value.
How do I pass a value between Postman requests?
Read it from the response in a post-response script and store it, for example `pm.collectionVariables.set('bookId', pm.response.json().id)`. Reference it later as `{{bookId}}` and clear it when the workflow ends if stale reuse would be risky.
Should Postman tests use a JSON Schema?
Schemas are useful for required properties, types, and nested structure. Keep them aligned with the real contract and avoid forbidding additional fields unless the API promises that restriction, because compatible additions should not necessarily break consumers.
How do I keep Postman API tokens secure?
Do not place real tokens in exported collection or environment values. Mark secrets as sensitive where available, use a secret manager or CI secret variables, generate short-lived tokens during the run, and avoid logging them.