QA How-To
Bruno vs Insomnia for API Testing (2026)
Bruno vs Insomnia for API testing in 2026: compare Git storage, assertions, collection runners, and CI with a repeatable local API testing walkthrough.
19 min read | 3,572 words
TL;DR
Bruno suits a repo-first API suite with reviewable collection files and a direct CLI. Insomnia suits teams that explore and design APIs in a shared workspace and automate saved requests with Inso CLI. Run the same success and error cases through both before choosing.
Key Takeaways
- Choose Bruno when collection files belong beside service code and need Git review.
- Choose Insomnia when API exploration, specifications, and a shared workspace guide testing.
- Assert response fields and expected errors, not only successful transport.
- Run Bruno with bru run and Insomnia with inso run collection to verify CI behavior.
- Prove a fresh-clone run and a deliberate assertion failure before migrating a suite.
- Keep secrets out of collection files and define one canonical Insomnia data source.
Bruno vs Insomnia for API Testing is a decision about how your team creates, reviews, and runs API requests. Choose Bruno when collection files should live beside service code and pass through normal pull request review. Choose Insomnia when API exploration, specification work, and a shared client workspace are central to testing. Both support scripted assertions and command-line runs, so test a real success and failure path before moving a large suite.
This guide uses one local API in both clients. You will check a health response, create a valid order, and reject an invalid quantity. The local service avoids outages or data changes on a public demo API. Then you will run each collection from a terminal and inspect what a teammate would see in Git and CI. Commands assume a Unix-like shell and a supported Node.js installation; adapt directory commands for another shell.
TL;DR
| Decision point | Bruno | Insomnia |
|---|---|---|
| Storage | Local collection files suited to Git; Bru Lang remains supported, while OpenCollection YAML is recommended for new collections | Local, cloud, and Git Sync workflows; Git Sync writes YAML resources |
| Test script | test, Chai expect, and the res response API |
After-response scripts using insomnia.test, insomnia.expect, and insomnia.response |
| Batch runner | bru run for a request, folder, or collection |
App Collection Runner or inso run collection |
| CI evidence | Nonzero exit on failed request or test; JSON, JUnit, and HTML reporting | CLI exit status, console reporters, and JSON output |
| Best initial fit | Repository ownership and direct request-file review | Exploratory API work and a shared design/testing workspace |
| Pilot risk | Desktop and CLI modes may differ for advanced scripts | Exported files may drift from desktop state unless storage is defined |
For a code-owned API suite, pilot Bruno first. For a team that explores imported specifications together, pilot Insomnia. Treat the table as a shortlist, not a verdict on your particular authentication method, protocol, or CI environment. Prove those requirements using the installed tool release.
What You Will Build
- A dependency-free local HTTP API with two endpoints.
- A successful health check and order creation.
- A negative case that expects HTTP 400 and a named error code.
- Equivalent Bruno and Insomnia assertions.
- A repeatable command-line run and one deliberate failure drill per client.
The order ID below is a deterministic fixture. It helps compare syntax without introducing shared database state. In a production suite, a create test should inspect a generated ID, query the resource, and clean it up. Here the important contract is whether a valid quantity is accepted and zero is rejected.
Prerequisites
Install Bruno Desktop and Insomnia Desktop from their official sites. Install a currently supported Node.js release and the Bruno CLI. Install Inso CLI using the official Inso CLI instructions for your operating system. Match the desktop app and CLI releases you actually use; record the versions in your project. This article avoids hard-coded package pins because a copied version could be wrong for your installed apps.
node --version
npm --version
npm install --global @usebruno/cli
bru --version
inso --version
Verify every version command prints a value. bru --help should show the run command and inso run --help should show collection execution. If a command is missing, fix the installation or PATH before continuing. Keeping a lockfile for your project and a documented Inso installation method is more useful than relying on whichever global package happens to be installed on a developer laptop.
Step 1: Start One Deterministic Local API
Create a directory named api-client-lab and save the following as server.mjs. The Node built-in HTTP server reads the request body, parses JSON, and validates a positive integer quantity. It returns a fixed ID for a valid order. Malformed JSON gets its own error code so you can later add a separate case without confusing it with invalid order fields. No external package or database is required.
import { createServer } from 'node:http';
createServer(async (request, response) => {
const send = (status, data) => {
response.writeHead(status, { 'content-type': 'application/json' });
response.end(JSON.stringify(data));
};
if (request.method === 'GET' && request.url === '/health') {
send(200, { status: 'ok' });
return;
}
if (request.method === 'POST' && request.url === '/orders') {
let body;
try {
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
} catch {
send(400, { error: 'INVALID_JSON' });
return;
}
if (!body || typeof body.sku !== 'string' ||
!Number.isInteger(body.quantity) || body.quantity < 1) {
send(400, { error: 'INVALID_ORDER' });
return;
}
send(201, { id: 'ord-demo', sku: body.sku, quantity: body.quantity });
return;
}
send(404, { error: 'NOT_FOUND' });
}).listen(4318, '127.0.0.1', () => {
console.log('API lab on http://127.0.0.1:4318');
});
Run node server.mjs in one terminal and leave it running. In another terminal, execute all three commands below. The health body must be {"status":"ok"}. The first POST must return HTTP 201 with ord-demo; the second must return HTTP 400 with INVALID_ORDER. The -i option prints HTTP status and headers, so you can verify the expected code independently of a client UI.
curl -i http://127.0.0.1:4318/health
curl -i -H 'content-type: application/json' -d '{"sku":"QA-BOOK","quantity":2}' http://127.0.0.1:4318/orders
curl -i -H 'content-type: application/json' -d '{"sku":"QA-BOOK","quantity":0}' http://127.0.0.1:4318/orders
If port 4318 is occupied, change it in the server and every request and command in this guide. When a client fails later, rerun the matching curl command first. That separates service availability from client configuration. For a larger fixture strategy, read API test data management.
Step 2: Save and Test a Bruno Health Request
In Bruno Desktop, create a collection named API Comparison Lab under the lab directory. Bruno now recommends OpenCollection YAML for new collections while continuing to support .bru files. Select Bru format if the app offers a format choice for this example, because the single-request terminal command below uses a .bru file. Let the app generate collection metadata. Verify its folder has bruno.json before invoking the CLI.
Create a GET request named Health with URL http://127.0.0.1:4318/health. In its Tests tab, paste this script and save. res.getStatus() reads the HTTP status and res.getBody() returns the parsed JSON response. A request that reaches some other 200 endpoint would fail the body check, which makes the assertion stronger than status alone.
test('health contract', function () {
expect(res.getStatus()).to.equal(200);
expect(res.getBody()).to.deep.equal({ status: 'ok' });
});
Click Send and inspect the Tests result. Then enter the collection directory in a terminal and run the saved request. The expected result is one request with a passing test and a zero exit status. The app may choose a filename other than Health.bru; inspect the directory and substitute its actual name. If the CLI says you are outside a collection root, the working directory or collection save location is wrong.
cd api-client-lab/'API Comparison Lab'
test -f bruno.json
bru run Health.bru
The file on disk can now be reviewed with the service change that motivated it. That is a practical Bruno strength only if reviewers can understand the assertion. Avoid a collection of requests that send successfully but never check application behavior. The API testing roadmap explains how request checks relate to integration and end-to-end coverage.
Step 3: Add a Bruno Success Case
Create Create Order, a POST request to http://127.0.0.1:4318/orders. Set its body type to JSON and enter {"sku":"QA-BOOK","quantity":2}. Make sure the content-type header is application/json; many clients add it for a JSON body, but inspect the actual outgoing request. Add this test. It asserts the 201 contract and the echoed fields. The fixed ID assertion is valid for this deterministic fixture only.
test('valid order is accepted', function () {
const body = res.getBody();
expect(res.getStatus()).to.equal(201);
expect(body.id).to.equal('ord-demo');
expect(body.sku).to.equal('QA-BOOK');
expect(body.quantity).to.equal(2);
});
Send the request and verify a 201 response with all three fields. From the collection folder, run its saved .bru file. The terminal name must match the file saved by your app. This focused command is useful when a broad collection run fails and you need to reproduce only the order case. A 201 status without field checks could hide a server that created the wrong SKU or quantity.
bru run 'Create Order.bru'
Do not copy the fixed ID pattern into a live create API. A real service may generate different IDs and timestamps on every run. Assert a nonempty ID of the expected shape, make a follow-up read request, and remove the created record or use a disposable tenant. A stable test checks the contract rather than accidental fixture values.
Step 4: Add Bruno Validation and CI Evidence
Create another POST named Reject Zero Quantity to the same URL, with body {"sku":"QA-BOOK","quantity":0} and the JSON content type. Paste this test. The 400 response is a passing outcome here: the service correctly rejects invalid input. The named error code makes the failure useful to callers; a generic check for any 4xx would miss a regression from validation to authorization or routing failure.
test('zero quantity is rejected', function () {
expect(res.getStatus()).to.equal(400);
expect(res.getBody().error).to.equal('INVALID_ORDER');
});
Send the negative request, confirm 400 and a green test, then run all three requests from the collection root. Bruno CLI returns a nonzero status for a failed request, assertion, or test. Produce a JUnit file for a CI artifact and check that it exists. The report should name health, accepted order, and zero quantity clearly enough for a teammate to diagnose a failure without opening the desktop app.
mkdir -p reports
bru run --output reports/bruno-results.xml --format junit
test -s reports/bruno-results.xml
Temporarily change the success test from expected status 201 to 200. Rerun bru run and verify the command exits nonzero, then restore 201. This one-time failure drill proves that your pipeline would stop on a broken contract. Keep generated reports out of source review. In CI, install the CLI using your team's tested version policy, start the service, wait for /health, and publish JUnit output as a job artifact. Bruno documents its CLI runner and reporting.
Step 5: Build the Equivalent Insomnia Collection
Open Insomnia and create an API Collection named API Comparison Lab. Add three requests with the same names and methods as the Bruno cases. Start with Health as GET http://127.0.0.1:4318/health. For this small lab, a literal loopback URL makes the comparison easy to inspect. When you target more than one environment, replace it with a named environment variable and verify which value CI selects.
Use the after-response script editor for new tests. Insomnia guidance recommends pre-request and after-response scripts. Its legacy unit test suites remain available for existing collections, but their tab is hidden by default in newer releases and deprecation is planned. The code below uses documented insomnia.response and insomnia.test APIs. Each run creates a named assertion visible in the results.
insomnia.test('health contract', () => {
insomnia.expect(insomnia.response.status).to.eql(200);
insomnia.expect(insomnia.response.json()).to.eql({ status: 'ok' });
});
Send the request. Verify the status is 200, the body is exactly the expected object, and the test passes. Run the terminal check below if the app result differs from Bruno. If curl passes while Insomnia fails, inspect the URL, active environment, proxy, and request method. Do not weaken the assertion to make a configuration error appear green.
curl -i http://127.0.0.1:4318/health
Insomnia can import Postman collections, OpenAPI specifications, cURL, HAR, and its own formats. That helps exploration but does not guarantee migrated tests keep their meaning. Inspect scripts and authentication after import. The API testing tool selection guide gives a wider checklist when these are not your only candidates.
Step 6: Add Insomnia Order and Error Assertions
For Create Order, choose POST http://127.0.0.1:4318/orders, JSON body {"sku":"QA-BOOK","quantity":2}, and the JSON content type. Put the following in its after-response script. Parse the response once, then assert the status and three body fields. Insomnia's API names differ from Bruno's, but the contract is identical. That equivalence is what makes this a fair tool comparison.
const createdOrder = insomnia.response.json();
insomnia.test('valid order is accepted', () => {
insomnia.expect(insomnia.response.status).to.eql(201);
insomnia.expect(createdOrder.id).to.eql('ord-demo');
insomnia.expect(createdOrder.sku).to.eql('QA-BOOK');
insomnia.expect(createdOrder.quantity).to.eql(2);
});
For Reject Zero Quantity, use the same POST URL with body {"sku":"QA-BOOK","quantity":0}. Save this distinct after-response script. It looks for 400 and the exact validation code. Do not share the createdOrder variable with another request: each after-response script should use its own response and be independently readable during a failure review.
const rejectedOrder = insomnia.response.json();
insomnia.test('zero quantity is rejected', () => {
insomnia.expect(insomnia.response.status).to.eql(400);
insomnia.expect(rejectedOrder.error).to.eql('INVALID_ORDER');
});
Send both requests. The valid request should show 201 and ord-demo; the invalid request should show 400 and INVALID_ORDER. Then run the two terminal checks to prove the server returns the same results without either API client. This catches a common pilot error: one tool silently points to staging while the other points to localhost.
curl -i -H 'content-type: application/json' -d '{"sku":"QA-BOOK","quantity":2}' http://127.0.0.1:4318/orders
curl -i -H 'content-type: application/json' -d '{"sku":"QA-BOOK","quantity":0}' http://127.0.0.1:4318/orders
Step 7: Run Insomnia from the App and Inso CLI
Open the collection menu, choose Run Collection, select the three requests, and order them Health, Create Order, Reject Zero Quantity. Run one iteration. Inspect the result for each request and its after-response test. The order is visible here, but this fixture has no state dependency; that is intentional. A production collection may chain IDs between requests, which requires cleanup and careful control over retries.
Export the collection in Insomnia YAML format as api-comparison-lab.yaml in the lab directory. Inso CLI accepts an export YAML file through --workingDir, and run collection accepts a collection identifier. Use the exact name shown in the app. Run from the directory containing the export, or replace the relative path with an absolute path. The --ci flag disables prompts; --reporter spec prints named results.
inso run collection 'API Comparison Lab' --workingDir ./api-comparison-lab.yaml --ci --reporter spec
Verify zero exit status and three selected requests with passing scripts. If Inso cannot find the collection, inspect the export and its identifier; do not switch to inso run test, which targets legacy test suites. The Inso collection command reference lists its working directory, environment, and reporter flags. The Insomnia import and export reference describes export formats.
Now break one expected value in the app, export again, and rerun the CLI. Confirm it fails and names the assertion, then restore the value and export once more. A green desktop run against changed requests does not prove that an older file committed to Git is green. This check exposes whether your chosen Insomnia storage workflow keeps CI and desktop state aligned.
Step 8: Compare Review and Reproduction Workflows
Make one small test change in each collection and inspect the Git diff. A Bruno collection uses local files, so its URL, body, and test script can sit in the same pull request as a service change. The exact format depends on what you selected: Bruno continues to support Bru Lang and recommends OpenCollection YAML for new collections. Judge the diff itself. Can a reviewer identify the request and changed contract without loading a desktop app?
Insomnia has local projects, cloud storage, exported YAML, and Git Sync. Git Sync writes YAML resources that work with Inso CLI and normal branch protections. It may also write metadata updates. Evaluate a real branch merge, not just the initial clean export. If you rely on exports, define who regenerates and commits the file after every app edit. A stale export is especially dangerous because the desktop app and CI can both pass while testing different requests.
Reproduce a colleague's run on a clean clone. Start the local service, install the chosen CLI by your documented process, load only committed collection data, and run the three cases. Record any app-only setup the command needs. The Postman vs Bruno comparison can help if you are migrating a Postman repository, and Postman Newman in CI gives another runner baseline.
Bruno vs Insomnia for API Testing: Bruno Strengths and Limits
Bruno's advantage is straightforward ownership of request files. A reviewer can inspect assertions alongside API code, and a developer can run only the failing .bru request from the terminal. bru run covers a whole collection or folder, while report options produce artifacts a pipeline can retain. In this lab, the health test is easy to isolate from the order cases. That short feedback loop matters when a service has many independent endpoints.
A file-first model does not solve secret handling automatically. Never commit live bearer tokens in a collection or environment file. Use the team's secret store and an override mechanism supported by the installed CLI; also inspect logs and generated reports for accidental exposure. CLI arguments may be visible to other local processes, so choose a secret injection path that fits your environment. Keep nonsecret base URLs and example data reviewable.
Bruno's current CLI documentation notes a safe-mode default in newer releases. Scripts requiring filesystem access or external packages may need explicit developer-mode configuration. The tests here use only the response API and do not need it. If your real collection uses advanced scripts, prove app and CLI behavior match before declaring a migration complete. A request passing interactively is not evidence that the release job can execute it.
Bruno vs Insomnia for API Testing: Insomnia Strengths and Limits
Insomnia combines request exploration, environments, optional OpenAPI specifications, and test scripts inside an API Collection. It supports importing several common formats, so a tester can quickly inspect an unfamiliar API. The Collection Runner orders saved requests, and Inso CLI can execute the same work in automation. Insomnia documents HTTP, GraphQL, gRPC, and WebSocket support; verify automated execution for the exact protocol and operation you need, rather than assuming interactive support means runner parity.
Storage choice is the key operational detail. A cloud project can ease shared exploration; a Git Sync project offers repository review; an export can serve a narrow CI job. Those paths have different permissions and freshness risks. If your team values code review, inspect Git Sync diffs and run CI against the committed files. If you use an export, automate or document regeneration. A change left only in desktop storage is invisible to the pipeline that reads yesterday's YAML.
Insomnia's older inso run test command is for legacy unit test suites. Its current testing guidance prefers after-response scripts for new cases. For the lab, inso run collection is the relevant command. Keeping those paths distinct prevents a misleading migration in which the app shows new assertions but CI runs only an old test suite. The GraphQL API testing guide provides deeper protocol-specific assertions if your API is not REST.
Which Should You Choose
Choose Bruno when the service repository is the natural owner of API contracts, engineers review collection diffs, and a direct CLI gate is enough. Require the pilot to prove a clean install, safe environment handling, and a nonzero job result on a broken assertion. For new collections, evaluate OpenCollection YAML as recommended by Bruno; keep existing Bru files when they serve the team well.
Choose Insomnia when people need a workspace for API discovery, spec inspection, and manual experimentation in addition to automated checks. Define the canonical storage before scaling. If CI consumes an export, keep that file synchronized. If CI consumes Git Sync data, test the clone and run from it. Ensure after-response tests travel with the requests and execute under Inso CLI.
For a mixed team, move one release-critical workflow through both tools. Compare setup steps on a fresh machine, diff clarity, failure messages, secret handling, and maintenance after an API change. Do not rank tools by elapsed time from three localhost calls: process startup and machine load dominate that tiny sample. Record the reasons for the choice and revisit them only when a requirement changes.
Troubleshooting
Bruno reports that you are outside a collection root -> Run from the folder containing bruno.json. Confirm the desktop app saved the collection to disk before retrying.
A Bruno single-request run cannot find its file -> Inspect the actual saved filename. Display names can differ from filenames, so pass the path on disk to bru run.
Inso cannot identify the collection -> Check --workingDir and open the exported YAML. Use the collection identifier if its display name is ambiguous.
The app passes while CI fails -> Compare collection files and selected environments. Re-export Insomnia changes or have CI read the Git Sync data that changed in the pull request.
The negative test is marked failed because it returned 400 -> Remove any blanket rule that every response must be 200. Assert the expected 400 and INVALID_ORDER for this request.
Requests refuse connections -> Confirm node server.mjs is running and curl -i http://127.0.0.1:4318/health works. Inside a container, loopback refers to that container, so use a reachable host for the API service.
Interview Questions and Answers
The structured questions below cover assertion design, file ownership, CI, and migration. Explain the exact contract each test proves. In particular, be ready to explain why the validation case is green when HTTP status is 400, and why the fixed ID check belongs only to this deterministic fixture. Use API testing interview questions for more practice.
Common Mistakes
- Comparing only the request editor. Include a fresh-clone run and a real Git diff; saved tests must survive beyond one laptop.
- Accepting any 2xx as success. Assert response fields and the expected status. The order response could contain the wrong quantity while still being 201.
- Copying scripts unchanged between clients. Bruno uses
res.getBody(); Insomnia usesinsomnia.response.json(). Translate the contract, not the method names. - Running a stale Insomnia export. Regenerate the file after edits or use a Git Sync workflow that CI reads directly.
- Committing secrets. A collection in Git is visible to reviewers and often to forks or logs. Keep live credentials in approved secret storage.
- Ignoring CLI mode. A desktop feature may depend on permissions or extensions unavailable to the runner. Test the actual CI command.
- Migrating all requests at once. Port a small high-value group and compare failure evidence before expanding coverage.
Where To Go Next
Replace the fixed order fixture with disposable test data, then add unauthorized, malformed JSON, and conflict cases. Keep each expected response explicit and clean up created resources. Separate a quick pull request gate from slower broad scenarios when the collection grows. The API testing roadmap helps place this work among other test layers. If you are preparing for a QA role, practice explaining your measured choice in interview practice.
Official references: Bruno CLI, Bruno Bru Lang, Insomnia testing, Insomnia scripts, Inso collection command, and Insomnia Git Sync. Check the installed release's documentation when upgrading because collection formats and CLI behavior can change.
Conclusion
Bruno vs Insomnia for API Testing comes down to which saved workflow your team can review and reproduce. Bruno is a strong fit for repository-owned request files; Insomnia is compelling when exploration, API design, and a shared workspace shape testing. Run the same success and validation cases in both, break an assertion deliberately, and choose the tool whose CI evidence and collection data your team can maintain.
Interview Questions and Answers
How would you choose Bruno or Insomnia for a new API suite?
I would identify where requests must live, who reviews them, which protocols and authentication flows matter, and how CI runs them. Then I would build one success and one error case in each tool. I would choose based on a clean-clone run and a deliberately failed assertion, not an editor screenshot.
What is the key difference between Bruno and Insomnia test scripts?
Bruno request tests use its `test` function, Chai `expect`, and the `res` response API. Insomnia after-response scripts use `insomnia.test`, `insomnia.expect`, and `insomnia.response`. I would migrate the asserted contract while rewriting calls for each tool's API.
Why does a 400 response pass the invalid-order case?
The request intentionally sends quantity zero, which violates the contract. The expected behavior is a 400 status and the `INVALID_ORDER` error code. A test that requires 200 for every request would mark correct validation as a failure.
How would you prevent Insomnia desktop and CI runs from drifting?
I would choose one canonical storage path, such as Git Sync files or a controlled export committed with the change. CI must run that same artifact. I would verify a fresh clone after editing a request and include the export or YAML diff in code review.
What does a Bruno JUnit report add to a pipeline?
It gives CI a structured artifact with named results that people and automation can inspect. The pipeline still needs the CLI exit code to fail the job when an assertion breaks. I would run a deliberate failure once to prove both the artifact and job result behave correctly.
How would you keep credentials out of collection files?
I would put nonsecret defaults in the collection and provide sensitive values through the CI secret store using a supported runtime override. I would review generated reports and logs for accidental token exposure. I would never commit a real production token in an environment file.
Why is a fixed order ID acceptable here but weak in production?
The local server is a deterministic fixture and always returns `ord-demo`, so the assertion proves the example response exactly. A production create endpoint should generate varying IDs. There I would validate the ID format, use it in a follow-up read, and clean up the created resource.
What evidence would justify migrating an existing collection?
I would show a specific gap such as difficult code review, unsupported runner behavior, or repeated CI failures that the candidate tool resolves. I would port a representative workflow and compare maintenance and failure diagnosis. If the existing suite works reliably, migration cost must be justified by a measurable improvement.
Frequently Asked Questions
Is Bruno or Insomnia better for API testing in Git?
Bruno is designed around local collection files that fit ordinary Git review. Insomnia also supports Git Sync, which writes YAML resources to a repository, but inspect the actual request and metadata diffs your team produces. A clean-clone CLI run is the deciding check.
Can Bruno run API tests in CI?
Yes. Bruno CLI runs requests and collections with `bru run`, returns a nonzero exit for failed tests or requests, and can produce JUnit output. Start the target API and select the intended environment before invoking the collection.
Can Insomnia run collections without opening the desktop app?
Yes. Inso CLI supports `inso run collection` using local Insomnia data or an export file through `--workingDir`. The file must include the requests and after-response scripts that CI is meant to execute.
Are Insomnia legacy unit tests recommended for new suites?
No. Current Insomnia guidance recommends pre-request and after-response scripts for new tests. Legacy unit test suites remain available for existing projects, but their tab is hidden by default in newer releases and deprecation is planned.
Does Bruno still support .bru files?
Yes. Bruno continues to support Bru Lang request files. Its current documentation recommends OpenCollection YAML for new collections, so evaluate that format when starting a repository.
How do I test an expected HTTP 400 in these tools?
Send a request with invalid input, then assert both status 400 and the documented error code. In Bruno, use `res.getStatus()` and `res.getBody()`; in Insomnia, use `insomnia.response.status` and `insomnia.response.json()` in an after-response script.
Should API client tests replace integration tests?
No. Collections are useful for deployed HTTP contract checks, while integration tests can exercise service components with controlled dependencies. Keep both where they catch different failures, and avoid duplicating an assertion without a reason.