When should a Cypress suite seed through cy.request() rather than cy.task()?
answer
- Ask what each door routes around
- One goes through the application, one past it
- Coupling follows whoever knows the schema
- Some states no endpoint will ever produce
- Keep the Node event list short and reviewed
basics
~20 sDefault to cy.request when an endpoint can express the state, because the record goes through the app's own validation and the session travels with it. Reserve cy.task for states no endpoint exposes and for work that is not HTTP.
solid answer
~40 sThey differ in how much of the application you route around. `cy.request()` goes through the real API, so the record it creates obeys the application's validation, defaults and side effects, and the cookies it collects land on the browser for the test identity. `cy.task()` hands an event and one serializable argument to a handler in Cypress's Node process, which can do anything Node can - write rows directly, restore a snapshot, invoke a CLI - and therefore bypasses every invariant the application enforces while coupling the suite to its schema. My default is HTTP unless the state genuinely cannot be expressed through it: a report aged six months, a half-finished reimbursement, a wholesale reset. I keep the list of task names short and reviewed, because each one is shared infrastructure somebody maintains.
go deeper
Know that both commands prepare state without the interface: cy.request calls a real endpoint, and cy.task runs code in Cypress's Node process.
Explain the mechanical differences that drive the choice - validation and cookies on one side, arbitrary Node access and one serializable argument on the other.
Argue the coupling cost concretely: a handler that knows the schema breaks on a migration with no API signal, and a directly written row can be a shape the product cannot produce.
Set the default and the review rule. Say what the team's standard is, which task names are sanctioned and why, and how you keep the Node-side surface from sprawling.
## Two doors, and what each one costs Cypress gives a spec exactly two ways to arrange state without driving the interface, and they are not interchangeable. - **`cy.request()`** speaks HTTP to a running server from Cypress's Node process. Whatever it creates goes through the application's own routing, validation and authorization, and any cookies the response sets land on the browser. - **`cy.task()`** hands an event name and one serializable argument to a handler running in the Node process that loaded your Cypress configuration. That handler can do anything Node can do: write to a database directly, drop a file on disk, call a cloud CLI, restore a snapshot. The judgement is not "which is faster". It is **how much of the application you are willing to route around, and what you take on by routing around it**. ## The case for the HTTP door Prefer `cy.request()` when an endpoint can express the state you need. - The record it creates is a record the application believes in. Required fields, defaults, derived totals and side effects such as an approval event all happen the way production does them. - It cannot drift out of step with the schema, because it does not know the schema. A migration that renames a column breaks the application and the seed at the same time, rather than leaving a seed that quietly writes rows nobody reads any more. - It carries the session: cookies the browser holds are attached, and `Set-Cookie` responses are written back, so the seeded state belongs to the identity the test will use. - The only knowledge it needs is the API surface, which the team already documents and versions. The costs are real but bounded: it can only reach states the API exposes, it is subject to whatever rate limits and validation the application enforces, and a multi-step setup becomes several round trips. ## The case for the Node door Reach for `cy.task()` when no endpoint can get you there, or when the setup is not HTTP at all. - States the product deliberately does not expose: an expense report already six months old, a reimbursement half-written by a job that crashed, a receipt whose file is corrupt. - Work that is not a request: restoring a snapshot, truncating tables, clearing a queue, invoking a database CLI, reading back what a download produced. - Anything that must persist between spec files, since the handler lives in one long-running Node process rather than in the browser. The costs are also real, and they are the ones teams underestimate: 1. **You bypass every invariant the application enforces.** A row written directly can be a shape the application can never produce, and the test then proves the application handles a state that cannot occur. 2. **It couples the suite to internals.** The handler knows the schema, the connection string, the file layout. Those change without an API version bump. 3. **It is a permanent extension point.** Each event name is a small piece of shared infrastructure somebody maintains, registered in Cypress's Node-events configuration. 4. **One serializable argument crosses the boundary**, so a rich setup becomes an ad-hoc payload format you now own. ## A rule of thumb worth defending | the state you need | door | why | |---|---|---| | something a user could create | `cy.request()` | the API already knows how, and enforces it | | something only time or a job produces | `cy.task()` | no endpoint exists to ask for it | | session or cookies for the test identity | `cy.request()` | cookies land on the browser automatically | | wholesale reset between runs | `cy.task()` | not an HTTP concern at all | | anything the interface must be proven to do | neither | drive it, or you are not testing it | The last row matters as much as the others. A backdoor that creates the very thing under test turns the test into a check that the read path renders a row. ## Standard-setting, not case-by-case choice Left to individual authors, both doors sprawl: one spec posts to five endpoints in sequence, the next writes rows directly because it was quicker that day, and nobody can say which states the suite is actually asserting on. The decision worth owning at team level is a default — *HTTP unless the state cannot be expressed through it* — plus a short, reviewed list of task names that are allowed to reach past the application, each with a stated reason. That keeps the Node surface small enough to understand and puts the burden of proof on the door that skips the application's own rules.
- What is the strongest argument against seeding an expense report with a direct database write?It can create a row the application itself could never produce. Required fields, derived totals and the events an approval emits are all skipped, so the test may assert that the product handles a state that cannot occur - and will keep passing after a change that breaks the real creation path.
- Is there state neither cy.request() nor cy.task() should be used to create?Yes: the thing the test is meant to prove. If the case is that submitting a receipt produces a pending report, seeding that report through either door reduces the test to checking that a read path renders a row. Backdoors belong to prerequisites, never to the behaviour under assertion.
saying these in an interview costs you the question
- Chooses the door by whichever was quicker to write
- Treats cy.task() as simply a faster cy.request()
- Seeds the very behaviour the test is meant to prove
- Lets every spec add task names with no review
- Ignores that direct writes can produce impossible states