In a Cypress suite, how much logic belongs inside a `cy.intercept()` route handler?
answer
- Declarative at one end, imperative at the other
- Ask what the handler is buying
- Compute, but do not decide
- Duplication can be documentation
- A test file is a poor place for a server
basics
~20 sEnough to compute a reply from what the request already carries, and no more. A Cypress handler that encodes validation, pagination or business rules becomes an unversioned second implementation of the service, hidden in a spec that nothing else tests.
solid answer
~50 sTreat the scale from `StaticResponse` to handler as a cost curve. A declarative stub shows its reply in the line you are reading and the Command Log's Routes panel documents it; a function hides the behaviour in code, and a bug inside it surfaces as a broken page rather than a failed assertion. My working rule is that handlers may **compute** but should not **decide**: deriving a forecast body from `req.query.station`, patching one field of a real response with `req.continue()`, or setting `req.alias` on a particular request are all fine. Encoding status-code policy, validation or pagination is a second implementation of the endpoint that no schema constrains and no backend test covers. Prefer one `StaticResponse` per test over one handler serving every test — the duplication is the documentation — and cap a handler at a size a reviewer will read.
go deeper
Start with the simplest form that works. If the reply never changes, a StaticResponse is the right answer and needs no defending.
Be able to argue both directions with a concrete example, and name what a handler costs in readability compared with a declarative stub.
Show how you would refactor an overgrown handler you inherited: what moves to per-test stubs, what becomes an assertion on the request, and what is deleted.
Own the rule the team reviews against, and defend it against the pull to build a convenient fake backend that no one owns and nothing keeps honest.
## What a route handler actually is A `cy.intercept()` route handler is a JavaScript function that lives in your spec, runs in the browser, and is invoked once for every matching request. That makes it enormously capable — it can branch on `req.body`, keep a counter across calls, read a module you imported, and compute a different reply each time — and it is exactly that capability that makes "how much of this should we do?" a real decision rather than a style preference. The scale runs from declarative to imperative: - `cy.intercept('GET', '/api/stations', { fixture: 'stations.json' })` — the reply is visible in the line you are reading. - `cy.intercept('GET', '/api/forecast*', (req) => req.reply({ body: byStation[req.query.station] }))` — one dimension of variation, still readable at a glance. - a forty-line handler with a mutable store, a switch on `req.method`, a hand-rolled pagination cursor and validation of the posted body — a small server, written in a test file. ## What the expensive end costs | Cost | Why it bites | |---|---| | **Opacity** | The Command Log's Routes panel shows the matcher, not the logic. A `StaticResponse` route is self-documenting; a handler hides its behaviour in code you have to read. | | **Debuggability** | A bug in a handler surfaces as a wrong page, a request error, or a completion error - not as a failed assertion pointing at the test's actual subject. | | **Ownership** | Behaviour encoded in a handler is a second implementation of the endpoint that no backend test covers and no schema constrains, so it drifts silently. | | **Reuse pressure** | A handler that grows gets shared, then parameterised, then depended on, and a test-only server becomes a component with no owner. | | **Concurrency** | State kept across calls inside a handler is per-test state; a spec that reorders or retries can see it in a shape you never designed. | None of these are arguments for never using a handler. They are arguments for being able to say what a given handler is buying. ## Questions worth asking before adding a branch 1. **Does the reply genuinely have to vary per request?** If two tests need two different fixed replies, that is two `cy.intercept()` calls in two tests, not one handler with an `if`. 2. **Is the varying part the subject of the test, or scenery?** Computing the forecast body from `req.query.station` is worth it when the test is about station switching. When the test is about the alerts banner, one canned station list is enough. 3. **Would the assertion be clearer than the branch?** A handler that inspects `req.body` and replies differently often wants to be an assertion on the request plus a fixed reply. 4. **Who reads this in six months?** If explaining the handler takes longer than explaining the feature, the handler is the wrong size. 5. **Is this behaviour the real service's, or this test's?** Reimplementing validation, auth or pagination is the boundary being crossed; shaping one field is not. ## Where the line usually falls A defensible default is: **handlers may compute, but should not decide.** Deriving a body from a value the request already carries, patching one field of a real response with `req.continue((res) => { ... })`, or setting `req.alias` so one request in a busy route is identifiable — all of these compute. Encoding what the server *should* do — status-code policy, validation rules, business logic — is deciding, and it belongs to the service and its own tests. Two practical rules keep suites on the right side of it: - Prefer a `StaticResponse` per test over one handler that serves every test. The duplication is the documentation. - Cap the handler at a size a reviewer will actually read. When it stops fitting on a screen, ask what it is doing that the test could assert instead. ## What this question does not decide It is worth being explicit about the boundary. Whether the suite should fake the network at all, which layer the fake belongs at, whether fixtures are recorded or hand-written, and how you detect drift against a real backend are separate decisions with separate owners. This one takes faking as given and asks only how much behaviour a Cypress route handler should carry once you have chosen to write one.
- What is a concrete sign that a Cypress route handler has grown too far?State that outlives a single request. A counter that changes the reply on the second call, a mutable list mutated by a stubbed POST and read back by a stubbed GET, or a switch on `req.method` are all signs the handler has become a service. It is now something a reviewer must simulate mentally to predict a test's outcome, which is the property tests exist to remove.
- Is a big shared handler in a Cypress support file better than repeating small stubs?Usually not. A shared handler becomes an implicit dependency of every spec: a change made for one test quietly alters others, and a failing spec no longer tells you where its data came from. Sharing the *payloads* is cheap and safe; sharing the *behaviour* couples specs together. Keep the route declaration next to the test that depends on its shape.
saying these in an interview costs you the question
- Treats a handler as strictly better than a StaticResponse
- Keeps mutable state across requests without noticing
- Reimplements server validation inside a spec
- Shares one giant handler across every spec
- Cannot say what the handler buys over a fixture