An uncomfortable question for your next engineering meeting: where does your API request collection actually live? In a lot of teams the honest answer is “in somebody’s Postman account”. Not in the repository, not in a backup, nowhere the company controls. When that person leaves, the living documentation of how to call your own system leaves too — and someone will rebuild the payload for that payments endpoint by hand.
This is not a listicle of ten Postman alternatives with star ratings. It is an argument about one specific problem — API collections that do not live next to the code — and about the tool that solves it by design rather than by configuration. If your complaint about Postman is pricing or telemetry, the reasoning still helps; but the strong case for Bruno is a different one.
Bruno is an API client that stores each request as a plain text file inside your project folder, versioned by Git, with no account and no mandatory cloud.
The problem is not price, it is where the collection lives
Postman is a good, mature tool. What changed was not its quality but its architecture: from version 10 onward the product revolves around an account and a cloud workspace. The most-used offline mode, Scratch Pad, was discontinued — Postman itself announced the sunset, cutting it from new downloads on May 15, 2023 and ending the deprecated mode on September 15 of the same year, replaced by a lightweight client for people who just want to fire requests.
The side effect is organizational, not technical. When the collection lives in someone’s account:
- It never gets reviewed. Nobody opens a pull request to discuss that the auth header changed.
- It drifts from the code. The endpoint was renamed on Friday; the collection still carries the old name eight months later.
- It belongs to whoever created it, not to the company. That is a continuity risk hiding in plain sight.
- It may carry secrets. A staging token pasted into an environment that syncs to a third-party server is a security decision nobody consciously made.
None of those problems is solved by paying more. They are solved by changing where the file sits.
How Bruno stores things
Bruno writes each request as a text file with a .bru extension inside a folder in your project. A collection is a directory; a folder in the UI is a folder on disk. There is no opaque local database, no sync, no login. The official repository, past 40,000 stars on GitHub, describes exactly that: a git-native API client.
A .bru file looks like this:
meta {
name: Create order
type: http
}
post {
url: {{base_url}}/v1/orders
body: json
}
headers {
Authorization: Bearer {{token}}
}
body:json {
{
"sku": "ABC-123",
"quantity": 2
}
}
It is readable by a human and, more importantly, readable by git diff. When someone changes the request body, the pull request shows the changed line. The collection stops being a binary attachment and becomes code — reviewable, with history, with an author and a date.
That changes one practical habit: the collection goes into the same repository as the service it exercises. Whoever clones the project already has the requests. Whoever joins the team on Monday opens the folder and sees how the system is called, without asking to be invited to any workspace.
Where to put the folder in the repository
The layout decision is small and it determines whether the pattern sticks. What works in practice is keeping the collection at the root of the service, next to the code it exercises:
orders-service/
├── src/
├── api/ ← the collection
│ ├── bruno.json
│ ├── environments/
│ │ ├── local.bru
│ │ └── staging.bru ← variable names only, never values
│ └── orders/
│ ├── create-order.bru
│ └── cancel-order.bru
└── .gitignore
Two rules avoid most of the pain later. First: one collection per service, not a monolithic company-wide collection — a collection shared across teams becomes the usual bottleneck, with merge conflicts in a file nobody owns. Second: the file name is the request name, in kebab-case, because that is how it shows up in the diff. Whoever reviews the pull request reads “create-order.bru” and already knows what to look at.
Licensing: what exactly is free
Worth being specific, because “open source” has become an elastic word. Bruno’s desktop app ships under the MIT license — the classic permissive license, with no restricted-commercial-use clause. You run it, modify it and use it inside your company without asking permission.
The company behind the project monetizes through a paid plan with additional features and enterprise support, and a paid lifetime edition existed before being discontinued. None of that is a prerequisite for normal use: there is no cap on requests, collections or people, because there is no server doing the counting. That is the difference between a free product and a product that has no way to charge by volume.
Bruno in CI: bru run
The part that usually decides adoption is not the UI, it is the CLI. The bru run command executes an entire collection from the terminal, runs the tests and assertions you wrote in JavaScript and returns an exit code — which is what a pipeline needs to fail a build. The CLI documentation covers report generation in JSON, JUnit and HTML, plus guides for GitHub Actions, Jenkins, GitLab CI and Azure DevOps.
The gain here is subtle and large: one artifact serves both uses. The request you fire by hand while debugging is the request that runs in the pipeline. There is no “development” collection and a separate “contract test” suite that drift apart in three months.
A typical test is a few lines:
test("creates order and returns id", function () {
expect(res.getStatus()).to.equal(201)
expect(res.getBody().id).to.be.a("string")
})
Secrets: the part that is easy to get badly wrong
Putting the collection in the repository raises the odds that someone commits a token. The correct pattern is short and non-negotiable: the versioned environment file carries only the name of the variable; the value comes from a local file ignored by .gitignore or from a secrets manager, injected as an environment variable at run time.
In CI, the value comes from the pipeline’s own secret store. At no point does the secret need to exist inside a .bru file.
Worth repeating what teams usually learn the expensive way: a token that landed in a commit is leaked, even if you delete it in the next one. The history is still there, and anyone who already cloned has a copy. The only answer is rotating the credential.
What you lose by leaving Postman
An honest migration states its cost. What Postman does better:
- Collaboration out of the box. Shared workspaces, history, comments and permissions come free with the account. In Bruno, collaboration is your Git workflow. For a team that lives in pull requests that is an advantage; for a team with product or QA people who do not use Git, it is real friction.
- Publishable documentation. Generating an API portal from a collection and sending the link to a partner is a case Postman solves in one click.
- Ecosystem and mocks. Mock servers, monitors and ready-made integrations have years of head start.
- Existing scripts. Import brings requests and environments across well; pre-request scripts and tests relying on Postman-specific APIs usually need manual review.
If your main use is precisely collaboration with people outside engineering, Bruno solves less than it promises.
How to decide in an afternoon
Do not migrate the whole team at once — that turns a reversible choice into a project. Pick one service, ideally one that already has a pipeline. Export the collection from Postman, import it into Bruno, drop the folder inside the service’s repository, point secrets at environment variables and add a bru run step to CI.
After two or three weeks, ask the three questions that matter: did anyone open a pull request changing the collection? Did the pipeline catch a contract break before deploy? Could a new joiner call the API without asking anyone for access? If all three answers are yes, the pattern proves itself and the remaining services follow — with no decision meeting, which is how good tooling changes tend to happen.