Collections & git
A Mándalo workspace is a directory of plain text files. Requests are .http files — the same format VS Code’s REST Client, httpYac and the JetBrains HTTP Client already read. That is the whole storage format: no database, no proprietary bundle, no export step before your work is reviewable.
The on-disk layout
my-workspace/
mandalo.toml
environments/
staging.toml
production.toml
collections/
payments-api/
collection.toml
list-charges.http
create-charge.http
ledger-balance.grpc
customers/
get-customer.http
Three rules explain the whole tree:
- A collection is a directory under
collections/containing acollection.tomlmanifest. - A folder is a plain subdirectory. Nesting is arbitrary.
- A request is one file, named after a slug of the request name. HTTP and GraphQL requests are
.http; gRPC requests are.grpc. Renaming a request renames the file. - Environments stay TOML — they are key/value tables, which is what TOML is good at.
Paths are confined to the workspace: a request path containing .., or a symlink pointing outside the collection directory, is rejected rather than followed. A file that fails to parse is skipped and reported, not silently dropped — the tree tells you which ones it could not read.
What a request file looks like
Everything the editor shows is a line you could have typed yourself. Nothing is encoded, escaped into a blob, or minified:
# collections/payments-api/create-charge.http
### Create charge
POST {{baseUrl}}/v1/charges
Content-Type: application/json
Authorization: Bearer {{token}}
{
"amount": 2400,
"currency": "eur"
}
> {%
pm.test("created", function () {
pm.response.to.have.status(201);
});
pm.environment.set("chargeId", pm.response.json().id);
%}
The pieces, in order: ### names the request. Then a normal request line, then headers one per line, a blank line, then the body. < {% … %} holds a pre-request script and > {% … %} a post-response script — REST Client’s own syntax, so a teammate with that extension can open and run the file unchanged.
GraphQL requests are .http too, marked with the X-REQUEST-TYPE: GraphQL header REST Client uses; the body carries the query and, after a blank line, the variables. gRPC gets its own .grpc extension in the same text style — a call line, metadata as header lines, and a JSON message as the body.
Why plain .http
The format was chosen for the diff and for the exit. JSON collections diff badly: everything is nested, so a one-field change drags brace and indentation churn along with it, and a reviewer cannot tell a URL edit from a reformat. A .http file has one thing per line, so a diff shows exactly what changed.
The exit matters just as much. .http is not our invention — it is what REST Client, httpYac and the JetBrains HTTP Client already read. A collection written by Mándalo is useful to a teammate who never installs Mándalo, and if you stop using it, your requests are not trapped in an export you have to convert.
Mándalo also writes files deterministically — same request, same bytes — and writes atomically, so an interrupted save never leaves a half-written request behind.
What a diff looks like
### Create charge
- POST {{baseUrl}}/v1/charges
+ POST {{baseUrl}}/v2/charges
Content-Type: application/json
+ X-Api-Version: 2026-08-01
{
"amount": 2400,
"currency": "eur"
}
A version bump and a new header. No wrapper format and no exported blob — that is a diff a reviewer can approve from a phone.
The review workflow
- Keep the workspace inside the repo that owns the API — either at the root or in something like
api/. - Edit requests in the app. Every save is a file write in your working tree; no sync, no publish button.
- Commit the changed request files alongside the handler code that changed. The endpoint change and its collection entry land in the same commit.
- Open the pull request. Reviewers see the API surface change as text, next to the implementation.
- Teammates
git pull. That is the entire sync protocol — there is no server to be out of date with.
Branching works because files work: a collection on a feature branch is exactly the requests that branch's API has. Merge conflicts are per-file and per-line, and you resolve them in the editor you already use.
Keeping secrets out of the repo
Environment files are plain text, so anything you type into a plain variable is committed as you typed it. Mark a variable as a secret and Mándalo stores the value in your OS keychain instead — the environment file records that the variable exists and nothing else. Check the file before your first commit; that is the cheapest habit on this page.
The workable pattern is to commit the shape of an environment, not its secrets:
- Commit environments that hold only non-secret values — base URLs, API versions, tenant ids, test account identifiers. These are the ones your teammates actually need.
- Leave secret variables absent from the committed file. Do not commit an empty placeholder either; an unresolved variable fails loud, which is the behaviour you want.
- Keep secret-bearing environments out of git entirely with a
.gitignorerule, and hand teammates the variable names rather than the values.
# .gitignore
environments/local.toml
environments/*.secret.toml
For automated runs there is a better answer than a file: the runner resolves any variable that the environment does not define from the process environment, under the name MANDALO_SECRET__<ENV>__<KEY>. An environment named staging using {{token}} reads MANDALO_SECRET__STAGING__TOKEN. That keeps the value in your CI secret store and out of every file. See CLI & CI. The command line runner works today, but it is not in the desktop installer yet — you build it from the repo.
Sharing outside git
When the other side does not have your repo, export the workspace as a single self-contained bundle file — collections, folder structure, requests and environments in one JSON document — and let them import it. It is the same importer the Postman import uses, and it reports exactly what it wrote.