What is Hurl?
Hurl is an open-source command-line tool, maintained by Orange under the Apache 2.0 license, that runs HTTP requests defined in a plain-text .hurl file. Each entry is a request written much as it appears on the wire, optionally followed by the expected response: a status line such as HTTP 201, headers to match, a [Captures] section that pulls values into variables, and an [Asserts] section of queries and predicates. It is built on libcurl, ships as a single binary, and doubles as an HTTP client for exploring an API and a test runner for checking it.
Why use it in the API Lifecycle?
- Readable, diffable tests: A
.hurlfile is plain text that looks like HTTP, so tests review cleanly in a pull request and need no language runtime or framework to run. - Request chaining:
[Captures]extracts values withjsonpath,xpath,headerand other queries, so a test can create a resource, capture its ID and fetch it in the next entry. - Rich assertions: Implicit asserts on status and headers, plus explicit predicates such as
==,exists,matches,isIntegerand thecountfilter for collection sizes. - Test mode:
hurl --testhides response bodies, reports per file and prints a summary. Files run in parallel by default in test mode. - CI reports:
--report-junit,--report-html,--report-tapand--jsonproduce output a pipeline can display. - curl semantics: Because it runs on libcurl, behavior around TLS, proxies, HTTP/2 and options such as
--connect-tomatches what engineers already know from curl.
Best Practices
- Keep
.hurlfiles in the API’s repository, one file per user journey (sign up, create order, cancel order), and runhurl --test --glob "tests/**/*.hurl"on every pull request. How this fits with contract and load testing is covered in API Testing. - Never hard-code hosts or tokens. Use
{{host}}and{{token}}templates and inject them with--variable,--variables-fileorHURL_VARIABLE_nameenvironment variables, and pass credentials with--secretso they are redacted from logs and reports. - Assert on the contract, not on incidental detail: check the status, the
Content-Type, and the specific fields a client relies on withjsonpath, rather than comparing whole bodies that change with every timestamp. - Make every file independent. Parallel execution is the default under
--test, so a file that depends on state created by another file will fail intermittently. Use--jobs 1only while you fix such a dependency. - Assert the failure paths too: a missing field should return
400 Bad Requestor422 Unprocessable Content, and an expired token401 Unauthorized. - Use
--retryfor endpoints that are eventually consistent, such as polling a job resource until it completes, instead of inserting fixed sleeps.