Le ticket arrive un lundi matin : « Migrer les collections Postman vers Bruno, les intégrer au mono-repo. » Postman, tu l'utilises depuis des années sans vraiment y penser. Bruno, tu en as entendu parler mais tu n'en as jamais ouvert. La doc officielle te liste les features. Les articles Medium t'expliquent "les 10 raisons de switcher". Aucun ne te dit concrètement comment démarrer proprement sur un mono-repo avec 6 services — patient API, labo, portail médecin, backoffice, facturation, base médicaments — 3 environnements et des secrets à ne pas commiter.
Voilà ce guide. Pas de teaser, pas de prise en main progressive — directement ce qu'il faut savoir pour que le résultat soit propre dès le premier commit.
Pourquoi Bruno
La vraie raison n'est pas "Bruno c'est mieux que Postman". C'est que Postman a dérivé vers un modèle cloud-first qui pose des problèmes concrets en équipe :
- Les collections Postman vivent dans le cloud Postman, pas dans ton repo. Résultat : elles ne sont pas reviewables dans une PR, elles ne suivent pas les branches, et si quelqu'un modifie une requête sans prévenir, tu ne le vois nulle part.
- Le partage de workspace nécessite un compte et une organisation Postman payante dès qu'on dépasse le plan gratuit — ce qui arrive vite à 4 devs actifs.
- Le client Postman pèse 300+ Mo (Electron). Ça reste anecdotique, mais c'est représentatif du modèle : faire grossir l'outil pour justifier la valeur cloud.
- Les variables d'environnement Postman sont stockées dans le cloud Postman, ce qui crée une friction pour les secrets : soit tu les mets là-bas (et tu perds le contrôle), soit tu les gères en dehors (et tu doubles la configuration).
Bruno résout ces problèmes d'une seule décision d'architecture : les collections sont des fichiers
texte (format .bru, une DSL simple) stockés dans un répertoire. Pas de compte, pas de
cloud, pas de sync magique. Un git clone et tout est là. Les changements de requêtes
apparaissent dans les diffs, les PR, le blame. C'est la seule vraie raison de migrer.
Les concepts en 5 minutes
Bruno a trois concepts à comprendre avant d'ouvrir l'interface. Deux sont similaires à Postman, un est différent de manière importante.
Collection — un répertoire de fichiers .bru. Quand tu ouvres Bruno
et que tu crées une collection, tu choisis un dossier sur ton disque. C'est tout. Chaque requête
est un fichier .bru dans ce dossier ou un sous-dossier.
Environment — un ensemble de variables nommées, définies dans l'UI Bruno et
stockées dans le répertoire de la collection dans un sous-dossier environments/, un
fichier .bru par environnement. Ces fichiers sont versionnés. Ils
contiennent les variables non-sensibles : URLs de base, ports, noms de domaine, feature flags.
Tu peux y mettre base_url = http://localhost:8080, tu ne dois pas y mettre un token
JWT ou un certificat client.
.env — un fichier .env classique à la racine de la collection,
gitignored. Il contient les secrets : tokens, mots de passe, chemins vers des
certificats. Bruno le charge automatiquement. Dans tes fichiers .bru, tu accèdes aux
variables d'environnement avec {{TOKEN}} (qui cherche d'abord dans l'Environment
actif, puis dans le .env) ou avec process.env.TOKEN dans les scripts.
| Concept | Versionné ? | Usage |
|---|---|---|
environments/*.bru |
Oui | base_url, ports, noms de domaine |
.env |
Non (gitignored) | Tokens, certificats, mots de passe |
.bru (requêtes) |
Oui | Définition des appels API |
Structure dans le mono-repo
Première décision : où mettre les collections dans le mono-repo. Plusieurs options :
postman/ (nom legacy à éviter), api-collection/ (verbeux),
api/ (ambigu si tu as déjà des packages Go qui s'appellent api),
bruno/ (nom de l'outil — explicite, court, sans ambiguïté).
Le choix bruno/ a un avantage supplémentaire : il rend visible dans l'arborescence
que c'est une collection Bruno, pas un répertoire de code Go. Quand quelqu'un clone le repo pour
la première fois, il n'a pas à deviner ce que contient ce dossier.
Structure recommandée pour 6 services :
bruno/
├── environments/
│ ├── local.bru
│ ├── dev.bru
│ └── prod.bru
├── .env # gitignored — tokens, certs
├── .gitignore
├── patient-api/ # Patient API — REST
│ ├── health.bru
│ ├── patients/
│ │ ├── create-patient.bru
│ │ └── get-patient.bru
│ └── ...
├── lab-service/ # Lab Service — gRPC
│ ├── lab-results.bru
│ └── ...
├── doctor-portal/ # REST
│ └── ...
├── backoffice/ # REST
│ └── ...
├── billing-service/ # REST
│ └── ...
└── drug-db-api/ # REST — API externe (ex: OpenFDA)
└── ...
Le .gitignore dans bruno/ contient au minimum :
.env
Un service = un sous-dossier. À l'intérieur, tu peux créer autant de sous-dossiers que nécessaire pour organiser les requêtes par ressource ou par feature. Bruno affiche l'arborescence telle quelle dans son explorateur.
Gérer les environnements
Les fichiers d'environnement sont des fichiers .bru avec une syntaxe légèrement
différente des requêtes. Voici à quoi ressemble un local.bru réaliste :
vars {
patient_url: http://localhost:8080
lab_url: localhost:9090
doctor_url: http://localhost:8081
backoffice_url: http://localhost:8082
billing_url: http://localhost:8083
drug_db_url: https://api.openfda.gov
grpc_tls: false
}
vars:secret [
AUTH_TOKEN
DRUG_DB_API_KEY
]
Le bloc vars:secret liste les noms des variables secrètes — il déclare leur existence
sans leur valeur. Les valeurs viennent du .env. C'est ce qui permet à Bruno de gérer
l'affichage (masqué dans l'UI) sans stocker les secrets dans le fichier versionné.
Le fichier dev.bru ressemble à ceci :
vars {
patient_url: https://patient-dev.acme-internal.com
lab_url: lab-dev.acme-internal.com:443
doctor_url: https://doctor-dev.acme-internal.com
backoffice_url: https://backoffice-dev.acme-internal.com
billing_url: https://billing-dev.acme-internal.com
drug_db_url: https://api.openfda.gov
grpc_tls: true
}
vars:secret [
AUTH_TOKEN
DRUG_DB_API_KEY
]
Pour switcher d'environnement dans Bruno : menu déroulant en haut à droite de l'interface, tu sélectionnes local, dev ou prod. Toutes les requêtes utilisent immédiatement les variables du nouvel environnement.
Les secrets dans .env
Le .env à la racine de bruno/ contient les valeurs réelles des variables
secrètes déclarées dans les environnements :
AUTH_TOKEN=
DRUG_DB_API_KEY=
CLIENT_CERT_PATH=/home/user/.certs/client.crt
CLIENT_KEY_PATH=/home/user/.certs/client.key
Dans une requête .bru, tu références ces variables avec la syntaxe double-accolades :
headers {
Authorization: Bearer {{AUTH_TOKEN}}
X-DrugDB-Key: {{DRUG_DB_API_KEY}}
}
Bruno résout {{AUTH_TOKEN}} en cherchant dans l'ordre : variables de l'Environment
actif, puis .env. Si tu as défini AUTH_TOKEN dans les deux, l'Environment
gagne. C'est le comportement attendu : tu peux surcharger un secret par environnement sans modifier
le .env.
Pour les certificats client (mutual TLS), Bruno supporte la configuration dans les paramètres
de la collection. Tu peux référencer les chemins depuis le .env :
{{CLIENT_CERT_PATH}} dans le champ de chemin de certificat.
gRPC en mode reflection
Le service de laboratoire d'Acme expose un service gRPC — les résultats d'analyses arrivent en
streaming au fil du traitement, dès qu'un automate termine une mesure. La bonne nouvelle : Bruno
supporte gRPC nativement, et il supporte le mode server reflection — tu n'as pas besoin
du fichier .proto pour découvrir et appeler les méthodes. Le serveur répond à une
requête de reflection et Bruno construit la liste des méthodes disponibles.
Pour que la reflection fonctionne, ton service Go doit avoir enregistré le service de reflection.
Si ce n'est pas encore le cas, deux lignes à ajouter dans main.go :
import "google.golang.org/grpc/reflection"
// Dans la fonction qui configure le serveur gRPC :
reflection.Register(grpcServer)
Voici à quoi ressemble un fichier .bru pour une requête gRPC unaire :
meta {
name: Get Lab Results
type: grpc
seq: 1
}
url: {{lab_url}}
body {
{
"patient_id": "{{patient_id}}",
"test_type": "blood_panel",
"date_from": "2026-01-01"
}
}
grpc {
method: LabResultService/GetResults
reflect: true
tls: {{grpc_tls}}
}
Le champ reflect: true dit à Bruno d'utiliser la reflection serveur pour récupérer
le schéma. tls: {{grpc_tls}} utilise la variable d'environnement — false
en local, true en dev/prod. L'URL est juste l'host:port sans scheme. Le
patient_id utilisé dans le body peut être injecté par un script pre-request
ou copié depuis une réponse REST précédente (création de dossier patient).
Pour le gRPC streaming (server-stream, client-stream, bidirectionnel), Bruno le supporte aussi mais la configuration est légèrement différente — la doc officielle couvre ce cas mieux que n'importe quel résumé.
Un vrai fichier .bru
C'est là où les gens sont le plus surpris en venant de Postman : les requêtes sont des fichiers texte avec une syntaxe simple, pas du JSON imbriqué sur 200 lignes. Voici une requête REST complète avec auth bearer, body JSON et headers custom :
meta {
name: Create Patient Record
type: http
seq: 1
}
post {
url: {{patient_url}}/v1/patients
body: json
auth: bearer
}
auth:bearer {
token: {{AUTH_TOKEN}}
}
headers {
X-Request-ID: {{$randomUUID}}
X-Client-Version: 2.1.0
}
body:json {
{
"last_name": "Dupont",
"first_name": "Marie",
"dob": "1985-03-12",
"nss": "{{$randomUUID}}"
}
}
tests {
test("status is 201", function() {
expect(res.status).to.equal(201);
});
test("has patient id", function() {
expect(res.body.patient_id).to.be.a("string");
});
}
Quelques observations sur ce format :
seqcontrôle l'ordre d'affichage dans l'explorateur Bruno.{{$randomUUID}}est une variable dynamique intégrée — Bruno en fournit plusieurs ($timestamp,$randomInt, etc.).- Le bloc
testsutilise Chai pour les assertions — même syntaxe que dans Postman si tu venais de l'ère pre-Postman v10. - Il n'y a pas de script pre-request en JSON/DSL bizarre — c'est du JavaScript dans un
bloc
script:pre-request { }.
Ce fichier est lisible dans un diff. Si quelqu'un change le body ou ajoute un header, tu le vois dans la PR. C'est la différence fondamentale avec Postman.
Migrer depuis Postman
Bruno supporte l'import depuis Postman Collection v2.1.
Ce qui migre bien : les requêtes REST simples, les headers, les body JSON/form-data, les variables
d'environnement non-secrètes. Bruno génère un fichier .bru par requête, l'arborescence
des dossiers est préservée.
Ce qui ne migre pas correctement :
- Scripts pre-request complexes — les scripts Postman avec des appels à
pm.sendRequest()ou des manipulations d'environnement avancées doivent être réécrits manuellement. La syntaxe Bruno est similaire mais pas identique. - Les Flows Postman — complètement Postman-spécifique, pas d'équivalent dans Bruno.
- Les monitors et mock servers — fonctionnalités cloud Postman, hors scope de Bruno.
- Les variables secrètes — normalement tu ne les exportes pas, donc elles n'apparaîtront pas dans le JSON exporté.
Recommandation pragmatique : importer une collection, vérifier le résultat, corriger ce qui est
cassé. Ne pas faire un import en masse des 6 services sans vérifier. Les requêtes critiques
(auth, création de ressources) méritent d'être recréées à la main en 5 minutes plutôt qu'importées
et laissées avec des comportements imprévisibles. Le format .bru est suffisamment
simple pour ça.
Le pas-à-pas
- Télécharger et installer Bruno — disponible sur usebruno.com/downloads pour macOS, Linux et Windows. Pas de compte à créer, pas de cloud, installation classique.
- Exporter depuis Postman — dans Postman, clic droit sur la collection → Export → Collection v2.1 → sauvegarder le fichier JSON quelque part en local.
-
Créer la collection Bruno dans le repo — dans Bruno :
File → Open Collection → choisir le dossier
bruno/dans le mono-repo (ou en créer un nouveau si le dossier n'existe pas encore). Bruno ouvre ce dossier comme racine de la collection. -
Importer la collection Postman — dans Bruno :
File → Import Collection → Postman Collection v2.1 → sélectionner le JSON exporté
à l'étape 2. Bruno crée un fichier
.brupar requête dans le dossier courant, en préservant l'arborescence des dossiers. -
Créer les environnements — dans Bruno : menu Environments
(icône engrenage en haut à droite) → Create Environment → nommer
local. Ajouter les variables :base_url, ports, etc. Répéter pourdevetprod. Bruno crée les fichiers correspondants dansbruno/environments/. -
Créer le .env et le .gitignore — créer
bruno/.envavec les secrets (tokens, clés API, chemins de certificats). Créerbruno/.gitignoreavec au minimum.envdedans. Committer le.gitignore, ne jamais committer le.env. -
Vérifier et corriger — tester chaque requête importée une par une.
Corriger les variables si nécessaire (
{{variable}}en Postman est compatible avec Bruno, mais les scripts pre-request complexes doivent être réécrits à la main dans un blocscript:pre-request { }en JavaScript pur). -
Committer —
git add bruno/→git commit -m "feat: migrate API collections from Postman to Bruno". Les fichiers.bru,environments/*.bruet.gitignoresont versionnés. Le.envne l'est jamais.
Conclusion
La migration Postman → Bruno prend une demi-journée pour un mono-repo de cette taille. La moitié
du temps c'est de la configuration initiale (structure, environnements, .env). L'autre
moitié c'est de vérifier que les requêtes importées fonctionnent vraiment.
Ce que tu gagnes n'est pas "un meilleur outil API". C'est que les collections deviennent du code : reviewables dans les PRs, diffables commit par commit, cohérentes entre tous les devs sans synchronisation manuelle. Quand quelqu'un ajoute un endpoint ou change un paramètre, c'est dans le même commit que le code Go.
C'est suffisant pour justifier la migration.