
Next.js Setting up Cypress
Of the four testing tools covered in this series, Cypress is the one that does double duty out of the box — genuine End-to-End testing and component-level testing, in one tool, with a visual, interactive test runner that's arguably its biggest practical advantage over the others. This article covers setting both modes up in a Next.js App Router project, plus the one real limitation worth knowing before you commit to it for component testing specifically.
The fastest path: a pre-configured example
npx create-next-app@latest --example with-cypress with-cypress-app
This scaffolds an entire project with Cypress already wired in — worth using directly if you're starting fresh and want a known-working reference configuration rather than assembling one from scratch.
Setting it up manually
npm install -D cypress
Add the interactive runner as an npm script:
// package.json
{
"scripts": {
"cypress:open": "cypress open"
}
}
Running it for the first time is itself part of the setup flow, not just "the test runner" — cypress open walks you through choosing E2E Testing, Component Testing, or both, and automatically generates a cypress.config.js file plus a cypress/ folder based on what you select.
One version constraint worth flagging directly, since it's the kind of error that looks like a config mistake rather than a version mismatch: Cypress versions below 13.6.3 don't support TypeScript 5 with moduleResolution: "bundler". If you're on an older Cypress version and hitting confusing TypeScript resolution errors specifically around this setting, upgrading Cypress itself — not touching your tsconfig.json — is very likely the actual fix.
Writing your first E2E test
Confirm your config looks like this:
// cypress.config.ts
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
setupNodeEvents(on, config) {},
},
});
With two simple pages to test navigation between:
// app/page.js
import Link from "next/link";
export default function Page() {
return (
<div>
<h1>Home</h1>
<Link href="/about">About</Link>
</div>
);
}
// app/about/page.js
import Link from "next/link";
export default function Page() {
return (
<div>
<h1>About</h1>
<Link href="/">Home</Link>
</div>
);
}
The test itself reads like a script of actual user behavior — because that's precisely what E2E testing is meant to simulate:
// cypress/e2e/app.cy.js
describe("Navigation", () => {
it("should navigate to the about page", () => {
cy.visit("http://localhost:3000/");
cy.get('a[href*="about"]').click();
cy.url().should("include", "/about");
cy.get("h1").contains("About");
});
});
Running E2E tests against a real build, not dev mode
This is worth taking seriously rather than skipping for convenience: Cypress genuinely drives a browser against your running application, and the recommendation is to test against your production build, not the dev server — dev mode's behavior (HMR, unoptimized bundles, different error handling) doesn't perfectly mirror what real users actually experience, and a test suite that only ever ran against next dev can pass locally while missing production-specific issues.
npm run build && npm run start
Then, in a separate terminal:
npm run cypress:open
Two small conveniences worth adopting rather than typing http://localhost:3000 in every single test: set baseUrl: 'http://localhost:3000' in cypress.config.js so you can write the shorter cy.visit('/') throughout your suite, and consider the start-server-and-test package, which lets one script boot the production server, wait for it to actually be ready, and then run Cypress — genuinely useful for CI, where you can't rely on a human manually starting two terminals in the right order.
Component testing: a genuinely different, lighter mode
Component tests mount one specific component in isolation — no full application bundle, no running server required — which makes them meaningfully faster to run than a full E2E suite, at the cost of testing something narrower.
Select Component Testing in the Cypress app and choose Next.js as the framework; this generates a cypress/component folder and updates your config automatically:
// cypress.config.ts
import { defineConfig } from "cypress";
export default defineConfig({
component: {
devServer: {
framework: "next",
bundler: "webpack",
},
},
});
// cypress/component/about.cy.tsx
import Page from "../../app/page";
describe("<Page />", () => {
it("should render and display expected content", () => {
cy.mount(<Page />);
cy.get("h1").contains("Home");
// Following the link itself is better suited to an E2E test than a component test
cy.get('a[href="/about"]').should("be.visible");
});
});
That comment in the test isn't incidental — it's the actual philosophical line between the two testing modes worth internalizing: a component test verifies this component renders correctly given its props/state; whether clicking a link inside it actually navigates somewhere is a cross-component, cross-page concern that belongs in E2E testing instead, not something a component test should try to verify itself.
Two limitations worth knowing before you lean on component testing heavily: Cypress currently doesn't support Component Testing for async Server Components — the same ecosystem-wide gap covered in this series' testing overview article, and the same fix applies: use E2E testing for those specifically. And because component tests deliberately don't spin up a real Next.js server, features that assume one exists — <Image />'s optimization endpoint being the most common example — may not function correctly out of the box in this mode.
Running headlessly, for CI
Interactive cypress open is great for local development and debugging, but CI environments need the headless equivalent:
// package.json
{
"scripts": {
"e2e": "start-server-and-test dev http://localhost:3000 \"cypress open --e2e\"",
"e2e:headless": "start-server-and-test dev http://localhost:3000 \"cypress run --e2e\"",
"component": "cypress open --component",
"component:headless": "cypress run --component"
}
}
cypress run is the headless counterpart to cypress open — same tests, no interactive UI, exactly the mode you want a CI pipeline actually invoking.
Key Takeaways
| Task | Command / config |
|---|---|
| Quickstart | npx create-next-app@latest --example with-cypress |
| Install manually | npm install -D cypress, then cypress open to scaffold config |
| E2E test location | cypress/e2e/*.cy.js |
| Component test location | cypress/component/*.cy.tsx |
| Run E2E against production | npm run build && npm run start, then cypress open |
| Headless (CI) | cypress run --e2e or cypress run --component |
| Known gap | No component testing support for async Server Components — use E2E instead |
Cypress's real strength in a Next.js project is consolidating two testing modes — E2E and component — into one tool with a genuinely excellent interactive debugging experience, at the cost of one specific, known gap around async Server Components that the whole testing-tool ecosystem currently shares. If that gap doesn't affect much of your codebase, or you're comfortable routing exactly that slice to E2E tests, Cypress alone can reasonably cover both layers of your test suite.


