In the previous article, we compared four API clients and concluded that Bruno was the most coherent choice for a development team in 2026. Now that we know why, let’s see how.
This article covers the essential features: organizing collections, managing variables, scripting, and e2e tests. It’s aimed at developers just starting with Bruno or who want to understand what it really offers.
Importing or Organizing a New Collection
Bruno supports importing from Postman, Insomnia, and OpenAPI. This is particularly useful when migrating from another tool. Existing collections aren’t lost. In this article, we’re starting from a blank collection. The .bru format developed by Bruno won’t be covered here since the team now recommends the YAML format implementing the OpenAPI Specification.
Just like in an IDE, the collection tree shown in Bruno represents the actual file tree on disk. Each request is represented by an independent file. With a clear structure and a readable format, Bruno lets you track file differences with Git naturally and without friction.
The file tree matches the request tree in Bruno. With a simpler format and a clear structure, Bruno makes it easier to track file differences with Git.
Variables
Before talking about scripting and tests, you need to understand how Bruno handles variables. It’s the foundation everything else rests on.
Like all API clients, Bruno offers different variable types:
Prompt variables: They force the user to enter a value before the request is sent. Useful for sensitive parameters or parameters that vary by context.
Request / Folder / Collection variables: These are equivalent to traditional variables found in all API clients. They’re defined statically and persist in files.
Environment variables: Just like in a regular project, Bruno supports variables described in a .env file that’s easily swappable depending on the target environment (local, staging, production).
Runtime variables: Variables assigned using scripts at execution time. They’re not directly modifiable by the user in the interface. They’re useful for automating tasks between requests, like propagating an authentication token. We’ll cover this in detail in the next section.
Here’s an image showing the variable usage priorities.
Bruno applies the same principle to variables as it does to requests: they’re stored in .bru or .env files, which keeps the tool Git-friendly. Nothing lives solely in memory or in an opaque cloud state.
Automating with Pre-request and Post-response Scripts
Once variables are in place, scripting allows you to manipulate them automatically between requests. This is where Bruno really distinguishes itself from Postman.
Bruno lets you write scripts to modify variables or requests using JavaScript syntax. We’ll look at two concrete cases: an authentication request and a project creation request.
Storing the jwt_token Using a Post-script in a Variable
For this example, we have the following request.
In Postman:
Here, you overwrite the jwt_token variable value in the collection. The previous value, which might have been set manually by a developer, is lost without warning.
javascript
pm.collectionVariables.set("jwt_token", pm.response.body.access_token)
In Bruno:
Here, you only modify the runtime value of the jwt_token variable. The old static value is preserved in the file. The runtime value is lost when you close the application, which is precisely the expected behavior for a temporary token.
javascript
bru.setVar("jwt_token", res.body.access_token)
This distinction isn’t trivial. In Postman, a script can silently overwrite a value someone had set for their tests. In Bruno, the two coexist without stepping on each other.
Bruno also offers a way to simplify this assignment using a visual post-response variables interface, which lets you replace the script above with a declarative configuration directly in the editor without writing a single line of code.

Variable Post Response to replace script
Post Response Variables to replace a script. Next, you just need to use this variable in your authentication system after executing the previous request.
Using a Runtime Variable as a Default Value for a Request
Here’s a more advanced use case with two requests: a POST and a GET on a project resource.
Here’s the project POST that returns the newly generated project ID.
Project POST Request. The POST creates a new project and returns its identifier. In post-response, we store this identifier in the autoProjectId runtime variable. This variable can then serve as the default value for the GET project by identifier: a pre-request script checks if a projectId variable exists in the collection. If not, it automatically injects autoProjectId as a fallback value.
Project GET Request with pre-request to assign the projectId. This mechanism is particularly useful when creating entities that require many parameters. It lets a developer run a sequence of requests without manually entering each generated identifier. Bruno takes care of it between steps. Here’s an example of a pre-request for project creation.
We see pre-request variables, and if they don’t have a value, we build a default value.
E2E Tests
Scripting covers variable automation. Tests, meanwhile, serve to validate that endpoints behave as expected.
Like Postman, Bruno lets you set up collections of endpoint tests. During execution, test reports can be generated to keep a record of results.
How to Write Them?
Bruno offers two approaches for writing tests, depending on the complexity of the case you’re covering.
The assertions table for simple, declarative checks without code. JavaScript scripts via Chai.js for more complex cases that require logic, loops, or conditions.
The Test Script
Bruno sets up test scripts in JavaScript, accessible in a dedicated “Tests” section. The assertion system is based on Chai.js, which makes tests readable by the widest audience, including developers unfamiliar with testing tools.
javascript
test("should have 'bonjour' as response", function () {
expect(res.body).to.be.equals("bonjour");
});
For more complex cases involving loops or conditions:
javascript
test("should validate user items by name", function () {
const body = res.body;
expect(body.length).to.be.gte(0);
body.forEach(item => {
if (item.owner === 'Benjamin') {
expect(item.item_id).to.be.equals('Foo');
}
});
});
The Assertions Table
To simplify writing the most common cases, Bruno provides an “Asserts” section with a declarative table interface that doesn’t require writing JavaScript. The first test above can be transcribed in just a few clicks.
However, the second test involving a forEach loop isn’t achievable through assertions. It’s a good indicator for knowing when to switch to a full script: as soon as you need to iterate or condition checks, the script becomes necessary.
Bonus: Response Query
A complementary tool deserves mention. It lets you extract elements from a response the same way you’d do with lodash. For example, to extract all values of the owner attribute in the following response:
json
[
{
"item_id": "Foo",
"owner": "Benjamin"
},
{
"item_id": "Bar",
"owner": "Olivier"
}
]
Just call res('..owner') to get ["Benjamin", "Olivier"], then verify that the expected value is in the list. This operator works in both assertions tables and test scripts.
javascript
res('..owner') = ["Benjamin", "Olivier"]
Then, it’s just a matter of verifying what you want, here whether “Benjamin” is actually included in the list or not.
Generating Test Reports
Two options are available for generating reports: via the interface in Bruno’s paid version, or via the command line thanks to the Bruno CLI, accessible for free.
To run them, just execute the following command:
bash
bru run your-folder-who-contain-test -r --reporter-html ./result.html --env-file ./environments/YourEnvironmentIfNeeded.br
The generated report is a standalone HTML file, readable in any browser, that summarizes all tests executed with their status. It’s a deliverable that can be directly shared with a QA team or client without additional infrastructure.
More info: https://docs.usebruno.com/bru-cli/overview
Conclusion
Bruno lives up to its promises. Behind a sober interface lies a tool that made clear technical choices and sticks to them.
Git integration isn’t a marketing argument: it concretely changes how you work as a team. Being able to review a request like you review code, seeing exactly what changed between two versions of an endpoint, including collections in the same pull requests as the code that tests them. These are real gains that accumulate over a project’s lifetime.
The assertions and response query lower the barrier to entry for writing endpoint tests. You don’t need to master Chai.js to cover 80 percent of common cases. And when cases become complex, the full JavaScript script steps in without friction.
Finally, the free CLI and HTML reports are a major selling point. Generating a readable test report without paying for a subscription, integrating it into a CI/CD pipeline, sharing it with a team. That’s exactly what you expect from a developer-oriented tool.
Bruno doesn’t revolutionize the field. But it cleanly solves frictions that many had accepted as normal.










Top comments (0)