Test the API first: why UI-first automation is the most expensive mistake in QA
Almost every team we meet automated the UI first. It is an understandable instinct — the UI is what you demo, what customers touch, and what looks like the product. It is also, measured over a year, the most expensive coverage you can buy.
The arithmetic nobody runs beforehand
Take one business rule: an order must be rejected when the requested quantity exceeds available stock.
Automated through the UI, that test logs in, navigates to a product, sets a quantity, submits, and asserts an error message. Perhaps eight seconds when the environment is healthy. It can break because of a layout change, a slow environment, a session quirk, a modal that appeared, or a locator that moved — none of which have anything to do with the rule under test.
The same rule at the API layer is one request and one assertion. Perhaps 200 milliseconds. It breaks when the rule changes, or when the contract changes. That is it.
Now multiply by the fifteen boundary cases that rule genuinely has — quantity zero, negative, exactly at stock, one above, fractional, enormous, non-numeric, missing. Fifteen UI tests is two minutes of runtime and fifteen things coupled to your markup. Fifteen API tests is three seconds and fifteen things coupled to a contract you version deliberately.
Teams do not usually write those fifteen UI tests. They write two, call it covered, and ship the boundary bug.
What the UI layer is uniquely for
This is not an argument against UI automation. It is an argument about order and proportion. UI tests are the only way to verify some genuinely important things:
- that the journey is wired together at all — a user can get from landing page to paid outcome;
- rendering, responsive behaviour and visual regressions;
- client-side state, routing, optimistic updates and browser-specific behaviour;
- accessibility in practice, in a real accessibility tree;
- anything where the interface is the product.
Note what is absent: business rules, validation logic, permissions, calculations, error handling. All of those live below the UI, and testing them through the browser means paying browser costs to verify server behaviour.
A shape that works in practice:
| Layer | Share of automated checks | Verifies |
|---|---|---|
| API / integration | ~70% | Business rules, validation, permissions, contracts, error paths |
| UI | ~20% | Critical journeys end to end, rendering, client state |
| Performance & non-functional | ~10% | Load behaviour, latency budgets, baselines |
The exact ratios matter less than the ordering instinct: push every check to the lowest layer that can genuinely verify it.
The second, larger benefit
API tests make your UI tests better, because they let you stop using the UI to create data.
Here is the usual pattern:
// 40 seconds of setup to test one settings screen
await signUpThroughForm(page, user);
await verifyEmailInMailbox(page, user);
await completeOnboardingWizard(page);
await createFirstProject(page);
await page.goto('/settings'); // finally, the actual test
Every step above is a chance to fail for a reason unrelated to what you are testing. And when it does fail, the report says the settings test broke — sending someone to investigate the wrong screen entirely.
With an API test layer you already have the primitives:
const { token } = await api.createUser({ onboarded: true });
const project = await api.createProject(token);
await page.goto('/settings', { storageState: sessionFor(token) });
// straight to the assertion
Four seconds instead of forty. One point of failure instead of five. And the failure message now means what it says.
How to sequence it from a standing start
If you have no automation at all, this order gets you a useful safety net fastest:
- Agree the critical journeys. Not all of them — the ones where a failure costs money or trust. For most B2B products this is five to ten.
- Automate the API paths those journeys depend on. Happy path first, then negative and boundary cases. This is where the bugs are.
- Add contract checks against your OpenAPI definition. Cheap to write, and they catch the breaking changes that silently break integrations.
- Build API helpers for test data. This is infrastructure, and it pays for itself in the next step.
- Now add UI tests for those same journeys, using the helpers from step 4 for setup.
- Wire it into CI with a real gate. Unenforced tests are a hobby.
- Then expand coverage into the long tail, still API-first.
Teams doing this in earnest usually have something trustworthy running within two weeks. Teams that start at step 5 spend two months building a suite that is slow, flaky, and quietly abandoned by the following quarter.
The objection worth taking seriously
“API tests pass while the product is broken for users.”
This is true, and it is why step 5 exists. An API suite with no UI tests can be fully green while a JavaScript error makes the checkout page unusable. You need both layers.
What you do not need is the same business rule verified fifteen times through a browser when one request would do. That is not thoroughness. It is a slow suite, a large maintenance bill, and — because someone eventually has to cut the runtime — the reason the boundary cases got deleted.