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 a collection.toml manifest.
  • 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

git diff — collections/payments-api/create-charge.http
  ### 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

  1. Keep the workspace inside the repo that owns the API — either at the root or in something like api/.
  2. Edit requests in the app. Every save is a file write in your working tree; no sync, no publish button.
  3. Commit the changed request files alongside the handler code that changed. The endpoint change and its collection entry land in the same commit.
  4. Open the pull request. Reviewers see the API surface change as text, next to the implementation.
  5. 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

Read this before you commit

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 .gitignore rule, 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.