
Turn curl Commands into Repeatable API Tests
Short answer: turn a curl command into a repeatable API test by importing it as a request step, adding assertions on the response, replacing pasted values with references to earlier responses, and running the file from the command line in CI. DevTools imports cURL commands in Studio, chains requests into reusable YAML, and its CLI exits with 1 when a flow fails.
This guide walks through those four steps with one login-then-fetch example, and then shows the route to take when you have a whole browser workflow instead of one command.
What a curl command lacks as a test
A curl command sends a request and prints whatever comes back. It does not judge the response. By default, curl does not consider HTTP response codes to indicate failure, so a 404 or a 500 still finishes as a normal run unless you add --fail, which makes curl return error code 22 for responses at 400 or greater. A shell script made of curl lines needs hand-written checks for every call.
Three gaps separate a curl command from a repeatable test:
- No assertions. Nothing says what a correct status, header or body looks like.
- No chaining. A token or an id copied from one response into the next command is pasted by hand, and it goes stale.
- No reporting. The result is text in a terminal, not an exit code and a report that CI can read.
The four steps below close those gaps in order: import the command, add assertions, chain the requests, and run the file in CI. DevTools is an open-source API testing tool that chains multi-step requests into reusable YAML, and it is the author's product.
Step 1: Import the curl command as a request
If the call exists only in your browser, copy it from there first. In the Chrome DevTools Network panel, right-click a request, hover over Copy and choose Copy as cURL, which copies the request as a cURL command. Use a request that returned the status you want the test to expect, since you are about to make it the baseline.
Then bring it into Studio. The DevTools import docs list the path: Import, then cURL, then Paste command.
The importer reads a documented set of curl flags:
| Flag | What it carries |
|---|---|
-X / --request | HTTP method |
-H / --header | Headers |
-d / --data | Request body |
-u / --user | Basic auth |
--url | URL |
-b / --cookie | Cookies |
A flag outside this list is not documented as imported, so check the request after the import and re-add anything missing by hand. The docs also say a cURL import must start with the curl command, so trim any shell prompt or leading text from what you paste.
Here is the command used in the rest of this guide:
curl -X POST https://api.example.com/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"demo","password":"demo-pass"}'
Replace the host with {{BASE_URL}} and the credentials with environment variables before you commit anything, so the same file runs against staging and local. The guide on keeping API tests in version control covers keeping those values out of Git.
Step 2: Add assertions so the request can fail
An imported request still passes whenever the server answers. Assertions are what make it a test. In a DevTools flow file, a request step carries an assertions list, and each entry is an expression on the response:
- request:
name: Login
method: POST
url: '{{BASE_URL}}/auth/login'
headers:
Content-Type: application/json
body:
username: '{{USERNAME}}'
password: '{{PASSWORD}}'
assertions:
- response.status == 200
- response.body.token != null
The first assertion pins the status you saw when you copied the command. The second checks that the response carries a token, without caring what its value is. Assert a handful of stable fields instead of the whole body, because timestamps and generated ids change on every run and make a test flaky. The guides on API assertions in YAML and deterministic assertions go further on which fields are safe.
Add one failing case to prove the check works. Change the expected status to 201, run the file once, and confirm it fails. Then change it back.
Step 3: Chain requests and pass values between them
Real workflows depend on earlier responses. A second curl command usually carries a token from the first, pasted in by hand. In a flow, you replace that pasted value with a reference to the earlier step's output. A later step reads an earlier step's output in the form {{NodeName.response.body.field}}, as in {{GetUser.response.body.id}}. Use depends_on to enforce execution order, so a step waits for the one it reads from.
For the login example, the fetch step reads the token from the Login step:
- request:
name: GetProfile
method: GET
url: '{{BASE_URL}}/api/me'
headers:
Authorization: 'Bearer {{Login.response.body.token}}'
depends_on: Login
assertions:
- response.status == 200
- response.body.id != null
The reference {{Login.response.body.token}} has the same shape DevTools generates when it detects a token flowing from a login response into a later request. Name each step for what it returns, such as Login instead of Request1, because the name becomes part of every reference after it.
The result is a two-step test that logs in, uses the token it just received, and fails if either response is wrong. For longer sequences, the API chain testing guide covers the pattern in depth.
When a workflow has many calls: record a HAR instead
Importing commands one at a time works for a handful of calls. For a whole workflow, such as sign in, create an item and check it, a recording is faster. Chrome can copy all requests as a chain of cURL commands, but a HAR import does more of the wiring for you.
When you import a HAR, the DevTools docs say it extracts the HTTP requests, creates a Flow showing request dependencies, detects data dependencies between responses and later requests, generates variable mappings, and adds response assertions. That removes most of the manual wiring from steps 2 and 3. Two details matter when you record. Turn on Preserve log in the Network tab so requests survive page navigations. And expect dependency detection to analyse JSON responses only.
A raw HAR holds secrets such as authorization headers and cookies. Redact it before you share it, using the guide on redacting HAR files safely, and keep the raw file out of Git. The HAR capture to CI gate post covers the full pipeline.
Step 4: Run the file in CI
With assertions and chaining in the YAML file, the CLI runs it. Install it with the install script, then run a named flow from the file:
curl -fsSL https://sh.dev.tools/install.sh | bash
devtools flow run ./tests/api-tests.yaml UserFlow
The first command is the install script; the second is the documented form of the run command. Add a JUnit report with --report junit:results.xml.
The exit code is what turns the file into a gate. The CLI exits with 0 when all flows succeeded, 1 when one or more failed and 2 on a configuration or setup error. A failed assertion fails the job with no extra scripting. The GitHub Actions workflow is in API testing in GitHub Actions, and the JUnit reports guide shows how to read failures in the pull request.
Failure modes to check before you trust the test
An imported command carries the values it had when you copied it, and those are the usual source of a test that passes today and fails next week. Three checks catch most of them.
Literal values that should be variables. A pasted token, a hard-coded id or a timestamp in the body belongs in an environment variable or in a reference to an earlier step. Search the file for any long opaque string and ask where it came from. If the answer is "a response", chain it as in step 3.
Steps that only pass in one order. If a request reads data that another request creates, give it a depends_on so the order is written down. A test that works because the steps happen to run in sequence on your machine will break when a runner schedules them differently.
Assertions that cannot fail. A check such as response.status != 500 passes on a 404. Run each assertion against a deliberately wrong expectation once, as in step 2, and confirm it fails. A test you have never seen fail is a guess.
When to keep a plain curl command
Not every call needs a test file. A curl line is still the quickest way to poke an endpoint while you are building it, to reproduce a bug report, or to paste into an issue so a colleague can see the same response. Keep using it there.
The test file earns its place when the call matters to the build: the login every other feature depends on, the create-and-fetch pair that guards your data model, the endpoint a customer integration calls. Start with the two or three calls whose failure would block a release, import them, and grow the file as bugs show you what else to cover. The REST API test cases checklist is a good guide to what to add next.
Common questions
Can I import a curl command that uses basic auth or cookies?
Yes. The documented flags include -u / --user for basic auth and -b / --cookie for cookies. Move the credentials into environment variables after the import.
What happens if the pasted text is not a curl command?
The import is not expected to work. The docs say a cURL import must start with the curl command.
Should I commit the HAR file?
Not the raw one. Commit the generated YAML and keep the capture local or redacted.
Conclusion
A curl command becomes a repeatable test when it gains three things: assertions that can fail, references that replace pasted values, and a run that returns an exit code. Import one command that you already use, add two assertions, chain the next call to it, and run the file in CI. Start with the call that breaks most often.
