REST API Testing, Step by Step
REST API testing in six steps: send one request, check the status, headers and body, chain calls, test failures, then run it on every pull request.

To test a REST API, send requests to its endpoints and check the status code, headers and body of every response, then chain the calls a real user makes and run them on every pull request. The order matters: one request first, then the status, the headers and body, a chain of calls, the failure cases, and last the automation. This post walks through those six steps against a small shop API, with a curl command for each, and ends with the same flow as a YAML test that runs in CI.
The six steps at a glance
REST API testing works on the layer behind the screen. SmartBear describes it as working with requests, responses, authentication, and payloads. If you want the wider definition first, read what API testing is. The steps:
- Send one request and read the whole response.
- Check the status code.
- Check the headers and the body.
- Chain calls: log in, create something, read it back.
- Test the failures on purpose.
- Turn the flow into a test that runs on every pull request.
Speakeasy puts the core of it as sending requests to your API endpoints and validating the responses. Steps 1 to 5 do that by hand. Step 6 makes it repeat.
Step 1: Send one request and read the whole response
The examples use a small shop API with a login route and an orders route. Set its base URL once, then send a GET:
export APP_URL=http://localhost:3000
curl -i "$APP_URL/api/orders/1"
Use -i so curl shows the status line and the headers above the body. A response has those three parts, and you should read all of them before you write a single assertion. This first request is you learning what correct looks like. Write down what you see: the status, the content type, the fields in the body. Those notes become the checks in the next two steps. If you want the same sequence in under a page, how to test an API compresses it.
Step 2: Check the status code
A status code is the cheapest check and the weakest. It tells you the route answered, not that it answered correctly. These five cover most of a first test set, as defined in the MDN status code reference:
- 200 OK: the request succeeded.
- 201 Created: the request has led to the creation of a resource.
- 400 Bad Request: the server would not process the request due to something it considered to be a client error.
- 401 Unauthorized: the request lacks valid authentication credentials for the requested resource.
- 404 Not Found: the server cannot find the requested resource.
Assert the exact code, not “any 2xx”. If your API answers a create with 201, a change to 200 is worth a failing test.
Step 3: Check the headers and the body
Headers carry the metadata. Check Content-Type first: a JSON route should answer with application/json, and a route that starts returning an HTML error page shows up here before it breaks a client.
Then check the body. For the request in step 1, a good answer looks like this:
HTTP/1.1 200 OK
Content-Type: application/json
{"id":1,"productId":42,"quantity":2,"status":"paid"}
Assert that the fields you rely on exist and have the right type. Compare the values you sent: an order created with quantity: 2 should answer 2. Leave out values that change on every run, such as ids and timestamps. For those, check only that the value is present and has the right shape.
Step 4: Chain calls: log in, create, read it back
A real user does not send one request. They log in, then create something, then look at it. Chain the calls the same way, carrying the token from the login response into the next request:
TOKEN=$(curl -s -X POST "$APP_URL/api/auth/login" \
-H 'Content-Type: application/json' \
-d "{\"email\":\"$TEST_EMAIL\",\"password\":\"$TEST_PASSWORD\"}" | jq -r .token)
curl -i -X POST "$APP_URL/api/orders" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"productId":42,"quantity":2}'
curl -s "$APP_URL/api/orders" -H "Authorization: Bearer $TOKEN"
The create should answer 201. The last call is the one that matters. A create can return a success code and store nothing, so follow every write with a read that must hold the new item. Stresseur’s generated flows do this: after a create (POST, PUT or PATCH), a later read of the list or page must hold the new id, or a value the user typed, made unique per run. The longer treatment of passing tokens between steps is in testing multi-step API flows with auth tokens.
Step 5: Test the failures on purpose
A flow is not tested until you have tried to break it. Send three requests that should be refused:
# no token: expect 401
curl -i "$APP_URL/api/orders"
# bad input: expect 400
curl -i -X POST "$APP_URL/api/orders" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"productId":42,"quantity":-1}'
# unknown id: expect 404
curl -i "$APP_URL/api/orders/999999" -H "Authorization: Bearer $TOKEN"
After the 400, list the orders again and check that the bad request saved nothing. A handler that writes the row and then refuses would pass a status-only check. The same cases written as code are in integration testing examples for a REST API.
Step 6: Turn the flow into a test that runs on every pull request
Typing curl proves the API works once. To repeat it, record the flow. Stresseur records how an API is really used during development, turns those flows into YAML tests, and runs them in CI on every pull request. The quickstart is npx stresseur init, which finds the app, starting its dev script if needed. It also has a built-in sample shop, which you run with --demo.
The flows live in the repo under api-tests/ as *.yamlflow.yaml files. They read the base URL from APP_URL, the variable you already used above. The login and create steps of the chain look like this:
- request:
name: Login
method: POST
url: '{{ #env:APP_URL }}/api/auth/login'
body: { email: '{{ #env:TEST_EMAIL }}', password: '{{ #env:TEST_PASSWORD }}' }
assertions:
- response.status == 200
- request:
name: CreateOrder
depends_on: Login
method: POST
url: '{{ #env:APP_URL }}/api/orders'
headers: { Authorization: 'Bearer {{ Login.response.body.token }}' }
body: { productId: 42, quantity: 2 }
assertions:
- response.status == 201
In CI, the workflow runs each flow 13 times, one warm-up and 12 measured. Each pull request gets one check, “Stresseur / performance”, and one comment. The full setup is in API tests in GitHub Actions, and what to automate first covers which flows to record first. A short subset that runs on every push is the subject of API smoke tests on every pull request. For the product side, see API testing with Stresseur.
What a failing step points to
Read the first failure before anything else, because the steps are in the order a bug tends to show up.
- Step 1 fails to connect. The base URL is wrong or the app is not running. Check
APP_URLbefore you suspect the API. - Step 2 returns the wrong status. The route exists but does something else. Compare with the notes you wrote in step 1.
- Step 3 returns the right status with the wrong body. A field was renamed, dropped, or filled with a default. This is the failure a status-only test misses.
- Step 4 fails right after a successful login. The token is not reaching the next call, usually through the header name or a missing
Bearerprefix. - Step 4 creates the item but the read does not show it. The write route and the read route disagree about where the data lives.
- Step 5 returns 200 where you expected 401, 400 or 404. This is the most serious result in the list: the API accepted something it should refuse.
Fix the earliest failing step first. A broken login makes every later step fail for the same reason, and the later failures say nothing new.
When to deviate from the order
The order suits an API you have not tested before. Change it when your situation differs.
- A public read-only API. Skip the login in step 4 and put the weight on steps 2 and 3.
- An API that changes daily. Stay with steps 1 to 5 by hand until its shape settles, then record the flow. A recorded test that you rewrite every day gets switched off.
- An API with many resources. Do not repeat all six steps for every route. Pick the path users depend on most, run the whole chain on it, and give each other resource a status and body check.
Conclusion
REST API testing is a short loop: send a request, check the status, check the headers and body, chain the calls, break it on purpose, then automate it. Do the first five steps by hand until you know what correct looks like, then record the flow so it runs on every pull request. Start with login and one create-and-read path, and add another test when a real failure shows what was missing.
Frequently asked questions
What is REST API testing?
REST API testing sends requests to a REST API's endpoints and validates the responses. SmartBear describes it as focusing on what happens behind the scenes, working with requests, responses, authentication, and payloads.
Do I need Postman to test a REST API?
No. Every step in this post works with curl, which sends the request and prints the answer. A recorded flow can then run the same steps in CI.
Which status code should a create request return?
201 Created, which MDN says indicates that the request has led to the creation of a resource. A plain 200 OK only says the request succeeded.
How do I test a REST API that needs a login?
Log in first, read the token from the response, and send it as a bearer token in the Authorization header of every later call. Then repeat one call without the token and check that it gets a 401, which means the request lacks valid authentication credentials.
How do I run REST API tests on every pull request?
`npx stresseur init-ci` writes the GitHub Actions workflow. Each pull request then gets one check named "Stresseur / performance" and one comment.