Thanks to visit codestin.com
Credit goes to lib.rs

21 releases (11 breaking)

new 0.12.0 Oct 30, 2025
0.10.0 Oct 17, 2025
0.7.1 Oct 29, 2024

#169 in Web programming

Codestin Search App Codestin Search App Codestin Search App Codestin Search App

792 downloads per month

MIT license

200KB
5K SLoC

Hen

Run API requests as files, from the command line.

name = Test Collection File
description = A collection of mock requests for testing this syntax.

$ API_KEY = $(./get_secret.sh)
$ USERNAME = $(echo $USER)
$ API_ORIGIN = https://lorem-api.com/api

$ ROLE = [admin, user, guest]

---

# Load other requests.
<< .fragment.hen

---

Some descriptive title for the prompt.

POST {{ API_ORIGIN }}/echo

* Authorization = {{ API_KEY }}

? query_param_1 = value

~~~ application/json
{
  "username" : "{{ USERNAME }}",
  "password" : "[[ password ]]",
  "role" : "{{ ROLE }}"
}
~~~

^ & status == 200

! sh ./callback.sh

Installation

cargo install hen

Usage

Usage: hen [OPTIONS] [PATH] [SELECTOR]

Arguments:
  [PATH]
  [SELECTOR]

Options:
  --export
  --benchmark <BENCHMARK>
  --input <KEY=VALUE>
  --parallel
  --max-concurrency <MAX_CONCURRENCY>
  --continue-on-error
  -v, --verbose
  -h, --help                   Print help
  -V, --version                Print version

Execute a Request

To execute a request, use the hen command on a file containing a request:

hen /path/to/collection_directory/collection_file.hen

This will prompt you to select a request from the file to execute.

Specifying a Request

You can specify the nth request directly by providing an index as the second argument:

hen /path/to/collection_directory/collection_file.hen 0

This will bypass the request selection prompt and execute the first request in the file.

Conversely, all requests can be executed with the all selector:

hen /path/to/collection_directory/collection_file.hen all

Selecting a Collection

Alternatively, you can specify a directory of collections. This will prompt you to select a collection file and then a request from that file.

hen /path/to/collection_directory

If the directory contains only one collection file, that file will be selected automatically, bypassing the prompt. Dotfiles (files starting with .) are ignored by the prompt.

Parallel Execution

Hen executes requests sequentially by default. Pass --parallel to run independent requests concurrently according to the dependency graph built from > requires: declarations.

hen --parallel ./collection.hen all
  • --max-concurrency <N> throttles the number of simultaneous requests. Omit or set to 0 to allow as many parallel requests as the graph permits.
  • --continue-on-error keeps running any branches that are unaffected by a failure. Without this flag Hen stops after the first failed request.
  • Outputs are still printed in plan order. Each request buffers its stdout until completion to keep logs deterministic.

Defining an API Request

An API request is defined in a text file with the .hen extension.

At a minimum, a request must have a method and a URL. The method is one of GET, POST, PUT, PATCH, DELETE, HEAD, or OPTIONS. The URL is the endpoint of the request.


[description]

METHOD url

[* header_key = header_value]

[? query_key = query_value]

[~ form_key = form_value]

[~~~ [content_type]
body
~~~]

[> requires: Request Name]

[& response_capture]

[ [^ assertion_condition] || "failure message"]

[! callback]

Headers

Headers are key-value pairs that are sent with the request. Headers are specified with the * character. For example,

* Authorization = abc123
* Content-Type = application/json

Query Parameters

Query parameters are key-value pairs that are appended to the URL. Query parameters are specified with the ? character. For example,

? page = 2
? limit = 10

Multipart Form Data

Multipart form data is used to send files or text fields with a request. Multipart form data is specified with the ~ character. For example,

$ file_1 = $(cat ./test_file.txt)
---

POST https://lorem-api.com/api/echo

# form data can be used to send text data
~ form_text_1 = lorem ipsum.
~ form_text_2 = {{ file_1 }}

# form data can also be used to send files
~ file_1 = @./test_file.txt

Request Body

The request body is the data sent with the request. The body is a multiline block specified with the ~~~ characters. The body can optionally be followed by a content type. For example,

~~~ application/json
{
  "key": "value"
}
~~~

User Prompts in Requests

User input can be requested interactively at runtime by using the [[ variable_name ]] syntax. A prompt may be used as a value for a query, header, form, or in the request body or URL. For example,

GET https://example.com/todos/[[ todo_id ]]

? page = [[ page ]]
* Origin = [[ origin ]]
~ file = @[[ file_path ]]

Prompts made in a request will be displayed in the terminal when the request is executed.

Supplying Prompt Values from the CLI

Use the --input flag to pre-fill prompt values and skip interactivity. Each --input expects key=value and may be repeated. Keys correspond to the placeholder name inside [[ ... ]].

hen --input foo=bar --input password=secret ./collection.hen

When a matching prompt is encountered, the provided value is used automatically. Any remaining prompts still fall back to the interactive dialog.

Response Captures

Response captures extract data from the response and store it in a variable for use in subsequent requests, callbacks, and assertions. The response is denoted with the & character, and can exposes the status code, headers, and body of the response.

& status -> $STATUS
& header.content-type -> $CONTENT_TYPE
& body -> $BODY

For JSON bodies, you can extract specific fields.

& body.token -> $TOKEN
& body.data.user.id -> $USER_ID
& body.items[0].name -> $FIRST_ITEM_NAME

Response captures update the callback execution context, making the captured variables available to any callbacks defined later in the same request, or in subsequent requests in the same collection.

Get JWT

POST {{ API_URL }}/jwt

~~~ application/json
{
  "username": "foo",
  "password": "[[ password ]]"
}
~~~

& body.token -> $TOKEN

! echo $TOKEN | cut -d '.' -f2 | base64 --decode | jq .

Referencing Dependency Responses

When a request declares dependencies with > requires:, it can also read from those responses using the capture syntax. Prefix the capture path with &[Request Name] to target the upstream response:

> requires: Get JWT

GET https://api.example.com/profile

&[Get JWT].body.token -> $TOKEN
&[Get JWT].status.code -> $LOGIN_STATUS

The same accessors (body, header, status) are supported. If a capture references a request that has not been declared as a dependency, Hen raises an error during parsing.

Assertions

Assertions provide lightweight response validation without leaving the request file. Each assertion line starts with ^ followed by a boolean expression. Assertions run after captures resolve and before callbacks execute. If any assertion fails, request execution stops and the failure message is surfaced.

POST https://api.example.com/login

& body.token -> $TOKEN
^ $TOKEN ~= /[A-Za-z0-9_-]+/ || "Token missing or malformed"
^ & status == 200

Available operators are ==, !=, <, <=, >, >=, and ~=. The ~= operator acts as a substring match for quoted strings and as a regular expression when the right-hand side is wrapped in / delimiters.

Assertions can reference the following values:

  • Response metadata: STATUS, STATUS_TEXT, RESPONSE, and DESCRIPTION.
  • Any response captures defined earlier in the request (e.g., $TOKEN).
  • Numeric and string literals (quoted or unquoted when numeric).
  • Response data directly via the capture syntax used elsewhere. For example: ^ & body.token == 'abc' or ^ &[Login].header.authorization ~= /Bearer/.

Optionally append || "custom message" to override the default failure text. Messages may interpolate collection or request variables via {{ variable }} before evaluation.

Callbacks

Callbacks are shell commands that are executed after a request is made. Callbacks are defined in the request definition with the ! character. For example,

GET https://lorem-api.com/api/user

# inline shell command
! echo "Request completed."

# a shell script
! sh ./post_request.sh

If a request has multiple callbacks, they are executed in the order they are defined, top to bottom.

GET https://lorem-api.com/api/user

# This is executed first
! echo '1'

# This is executed second
! echo '2'

Callback Execution Context

Callbacks are executed with response data passed as environment variables. The following environment variables are available to callbacks:

  • STATUS: The HTTP status code of the response.
  • STATUS_TEXT: The canonical reason phrase for the HTTP status code, when available.
  • RESPONSE: The response body of the request.
  • DESCRIPTION: The description of the request.

For example, the following callback will assert that the status code of the response is 200.

#!/bin/bash
# ./post_request.sh

if [ "$STATUS" -eq "200" ]; then
    echo "✅ [$DESCRIPTION] Received status 200"
    echo $result
else
    echo "❌ [$DESCRIPTION] Expected status 200 but got $STATUS"
    echo $result
fi
Echo body w. callback

POST https://lorem-api.com/api/health

! sh ./post_request.sh

Defining an API Collection

A file containing multiple requests is called a collection. Collections can be used to group related requests together.

Collections can start with a preamble that contains metadata about the collection. The preamble is separated from the requests by a line containing three dashes ---. The same line is also used to separate requests from each other.

name = Optional Collection Name
description = Optional Collection Description

[VARIABLES]

[GLOBAL HEADERS]

[GLOBAL QUERIES]

[GLOBAL CALLBACKS]

---

[request 1]

---

[request 2]

---

etc.

Global Headers, Queries and Callbacks

Any headers, queries or callbacks defined in the collection preamble become global and are included in all requests in the collection.

In the example below, the Authorization header and page query is included in all requests in the collection. When each request is executed and a response received, the callback echo "Request completed." is executed.

* Authorization = foo
? page = 2
! echo "Request completed."
---
GET https://api.example.com/users
---
GET https://api.example.com/posts

Global callbacks are executed before request-specific callbacks.

User Prompts in Collections

User prompts can be used in a collection preamble. The user is prompted when the collection is loaded: either directly via the CLI or as a prompt in the interactive mode.

$ foo = [[ bar ]]
---
POST https://lorem-api.com/api/echo

~~~ application/json
{ "foo" : "{{ foo }}" }
~~~

Prompt values can be supplied from the CLI using the --input flag, as described in the User Prompts in Requests section.

Global Variables

Global variables are key/value pairs defined in the preamble of a collection file with the $ character. For example,

$ api_origin = https://example.com
$ api_key = abc123
$ username = alice

Variables can be used in the request definition by enclosing the variable name in double curly braces. For example,

GET {{ api_origin }}/todos/2

* Authorization = {{ api_key }}

? username = {{ username }}

Variables can also be set dynamically by running a shell command. For example,

$ api_key = $(./get_secret.sh)
$ username = $(echo $USER)

Or by setting the variable interactively:

$ api_key = [[ api_key ]]

Request-Level Variables

Variables can also be declared inside an individual request. These request-level variables use the same $ name = value syntax and may appear before the HTTP method line or intermixed with other request statements. When both a global variable and a request variable share the same key, the request variable takes precedence for that request only.

$ api_origin = https://lorem-api.com/api
---
Fetch banner image

$ banner_text = Promo%20Time!
$ banner_fill = 444444

GET {{ api_origin }}/image?text={{ banner_text }}&fill={{ banner_fill }}

Request variables fully support shell substitutions ($(...)) and interactive prompts ([[ ... ]]), and are resolved using the same working directory as the collection. This makes it easy to specialize a base request without modifying the collection preamble or other requests.

Array Variables and Map Requests

Variables can also be defined as simple arrays by using square brackets:

$ AUTH_USERNAME = [foo, bar]

When a request references one or more array variables, Hen clones that request and executes it once per value (or per Cartesian combination when multiple arrays are used). Each clone receives the corresponding value injected everywhere the array variable appears—URLs, bodies, assertions, captures, callbacks, and so on. Every generated request is labeled with a bracketed suffix (for example, Echo a request [AUTH_USERNAME=foo]) and is logged, asserted, and benchmarked independently.

Key details:

  • Arrays must contain scalar values (no whitespace or nested arrays) and can be declared globally or inside a request. You can still compute them dynamically with $(...) commands or prompt interpolation.
  • A request may reference at most two distinct array variables and may expand into up to 128 total combinations. Hen raises a parse error if either limit is exceeded.
  • Map requests abort after the first failing iteration. Remaining iterations are skipped and reported as a MapAborted failure, even when --continue-on-error is set, ensuring side-effectful callbacks do not repeat.
  • Environment exports produced by each iteration are suffixed with the same bracket label (for example, TOKEN[AUTH_USERNAME=foo]) to keep values isolated.
  • Requests cannot depend on a mapped request; declare dependencies on a non-mapped helper if shared setup is required.

This behavior makes it easy to fan a single request across a table of inputs without duplicating boilerplate in the collection file.

Request Dependencies

Requests can depend on other requests in the same collection. Declare dependencies with the > directive placed before the HTTP method (or alongside other request-level declarations):

> requires: Get JWT

GET https://api.example.com/profile

Hen constructs a directed acyclic graph (DAG) from these declarations when it loads the collection. Each dependency must reference another request's description exactly, and any cycles cause an error. When you execute a request, Hen automatically runs its prerequisites first—even if you selected only a single request from the CLI.

All variables captured or exported by a dependency become part of the dependent request's context. That means placeholders like {{ TOKEN }} resolve using values captured upstream, and captures can reference dependency responses via &[Dependency].body... as described earlier. This makes multi-step workflows (login → reuse token → perform action) straightforward to model in one collection file.

Additional Syntax

Comments

Comments are lines that are ignored by the parser. Comments start with the # character. For example,

# This is a comment

GET https://example.com/todos/2

Fragments

Fragments are reusable blocks of text that can be included in multiple requests. Fragments are defined in a separate file and included in a request with the << character. For example,

# .fragment.hen

* Authorization = abc123
GET https://example.com/todos/2
<< .fragment.hen

Fragment paths can be absolute or relative to the collection file.

Fragments can contain multiple requests, headers, query parameters, and request bodies. Fragments can also contain variables and other fragments.

Conditional Guards

Prefix an assertion or fragment with a boolean guard to evaluate it only when the predicate resolves to true:

[ROLE == admin] ^ $STATUS == 200

[ROLE == admin] << admin.assertions.hen
  • Guards are parsed with the same expression grammar as assertions and can reference variables, captures, or response metadata (STATUS, RESPONSE, etc.).
  • Lowercase bare words are treated as string literals, so ROLE == admin compares against the literal value "admin".
  • Guarded assertions are skipped (and reported as such) when the predicate is false.
  • Guard prefixes on fragment includes apply to every statement in the fragment, so guarded fragments are only executed when the predicate passes.

Additional Features

Export Requests

Requests can be exported as curl commands. This is useful for debugging or sharing requests with others.

 $ API_URL = https://lorem-api.com/api

---

POST {{ API_URL }}/echo

~~~ application/json
{ "foo" : "bar" }
~~~
curl -X POST 'https://lorem-api.com/api/echo' -H 'Content-Type: application/json' -d ' { "foo" : "bar" }'

Exporting happens once all variables and prompts have been resolved and the request is ready to be executed. Callbacks are ignored during export.

Benchmarking

Requests can be benchmarked by specifying the --benchmark flag with the number of iterations to run. This will run the request the specified number of times and output the average time taken to complete the request.

hen /path/to/collection_file.hen --benchmark 10
Benchmarking request: Echo form data
[##################################################] 100.00%

Mean Duration: 399.95937ms
Variance (%): 0.6831308901940525

Notes:

  • Callbacks are ignored when benchmarking.
  • User prompts will still be executed in benchmarked requests, and so should be avoided, or used in the preamble only.

Examples

Basic Request

GET https://lorem-api.com/api/user/foo

Request with Headers, Query Parameters, and Form Data

POST https://lorem-api.com/api/echo

# Header
* foo = abc123

# Query
? bar = abc123

# Form Data
~ baz = abc123

Request with Body

POST https://lorem-api.com/api/jwt

~~~ application/json
{
  "username": "bar",
  "password": "qux"
}
~~~

Request with Callback

#!/bin/bash

if [ "$STATUS" -eq "200" ]; then
    echo "✅ [$DESCRIPTION] Received status 200"
else
    echo "❌ [$DESCRIPTION] Expected status 200 but got $STATUS"
fi
GET https://lorem-api.com/api/user

! sh callback.sh
! echo $JWT_TOKEN | cut -d '.' -f2 | base64 --decode | jq . -> $JWT_PAYLOAD

When using command -> $VARIABLE, surround the arrow with whitespace. The command still runs in the same environment, but its stdout is suppressed; only the sanitized output is saved into the named variable for later requests and callbacks.

Request with Assertions

$ IMAGE_TEXT = https%3A%2F%2Florem-api.com
$ FILL_COLOR = CECECE

GET {{ API_URL }}/image?format=png&fill={{FILL_COLOR}}&text={{IMAGE_TEXT}}

^ STATUS == 200
^ & header.content-type  == "image/png"

Request with Dependencies

$ API_URL = https://lorem-api.com/api

---

Get JWT

POST {{ API_URL }}/jwt

~~~ application/json
{
  "username": "foo",
  "password": "[[ password ]]"
}
~~~

& body.token -> $JWT_TOKEN

^ STATUS == 200
^ $JWT_TOKEN ~= /[A-Za-z0-9_.-]+/

---

Echo Response

> requires: Get JWT

POST {{ API_URL }}/echo

~~~ application/json
{ "foo" : "{{ JWT_TOKEN }}" }
~~~

^ & body.foo == $JWT_TOKEN

Mapped Request

$ REST_ORIGIN = https://lorem-api.com/api
$ USERNAME = [foo, bar]
$ ROLE = [admin, user, guest]

---

Echo a request

POST {{ REST_ORIGIN }}/echo

~~~ application/json
{
  "username" : "{{ USERNAME }}",
  "role" : "{{ ROLE }}"
}
~~~

^ & status == 200
^ & body.username ~= /bar|foo/
^ & body.role ~= /admin|user|guest/

Dependencies

~15–32MB
~409K SLoC