
Next.js Setting up Vitest
Vitest is the newer entrant among this series' four testing tools, built from the ground up around Vite's tooling rather than retrofitted onto an older bundler-agnostic design the way Jest originally was — and the practical result most teams notice first is meaningfully faster test execution, particularly as a test suite grows large. This article covers setting it up for unit testing in a Next.js App Router project, and the one limitation it shares with Jest that's worth knowing up front.
The same limitation as Jest, stated the same way
Vitest currently doesn't support unit-testing async Server Components, for the identical reason covered elsewhere in this series: the pattern is new enough to the broader React ecosystem that testing tooling hasn't fully caught up to it yet across the board — this isn't a Vitest-specific shortfall relative to its competitors. Synchronous Server and Client Components unit-test fine; for the async case specifically, reach for End-to-End testing via Playwright or Cypress instead.
The fastest path
npx create-next-app@latest --example with-vitest with-vitest-app
Setting it up manually
# TypeScript
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom vite-tsconfig-paths
# JavaScript
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom
The one package that differs between the two setups is vite-tsconfig-paths — worth understanding why it's TypeScript-specific rather than treating it as an arbitrary inclusion: it teaches Vitest to resolve module path aliases directly from your existing tsconfig.json, which is naturally only relevant when there's a tsconfig.json present to read from in the first place. A JavaScript project has no equivalent file for it to consult, so the package simply has nothing to do there.
// vitest.config.mts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [tsconfigPaths(), react()],
test: {
environment: "jsdom",
},
});
environment: 'jsdom' deserves a beat of explanation, because it's easy to treat as boilerplate rather than understand: Vitest's actual default test environment is node, which has no DOM APIs at all — no document, no window. Since you're testing React components that render into a DOM, you need jsdom (a JavaScript-based DOM implementation) standing in for a real browser. Omitting this setting doesn't produce a helpful error pointing at the missing environment; it tends to surface instead as confusing failures the moment your test touches anything DOM-related, since the code is executing against an environment that simply has no concept of a document to render into.
// package.json
{
"scripts": {
"test": "vitest"
}
}
Worth knowing before your first run, since it can be surprising if you're expecting Jest's default one-shot behavior: vitest watches for changes by default — running npm run test doesn't run once and exit the way jest alone does; it stays running, re-executing affected tests as files change. If you specifically want a single, non-watching run — the mode you'd actually want in most CI pipelines — you'd reach for vitest run instead of the bare vitest command.
Your first test
// app/page.tsx
import Link from "next/link";
export default function Page() {
return (
<div>
<h1>Home</h1>
<Link href="/about">About</Link>
</div>
);
}
// __tests__/page.test.tsx
import { expect, test } from "vitest";
import { render, screen } from "@testing-library/react";
import Page from "../app/page";
test("Page", () => {
render(<Page />);
expect(screen.getByRole("heading", { level: 1, name: "Home" })).toBeDefined();
});
Notice this uses the exact same React Testing Library APIs (render, screen.getByRole) as the Jest article in this series — Vitest and Jest share a near-identical testing API surface by deliberate design; the actual difference between them lives almost entirely in the underlying runner and its configuration, not in how you write assertions or queries day to day. If you already know Jest, most of what you already know transfers directly.
One structural flexibility worth knowing: while __tests__ is the conventional location shown here, test files can just as easily live colocated directly inside app/, next to the components they test — Vitest doesn't enforce one specific layout, and colocating tests alongside source is a genuinely popular alternative for teams who prefer keeping a component and its test in visual proximity rather than in a separate mirrored directory tree.
Running your tests
npm run test
Remember this runs in watch mode by default per the note above — for a single pass (CI, a pre-commit hook, anywhere you specifically don't want a long-running watcher), use vitest run explicitly instead.
Key Takeaways
| Task | Command / config |
|---|---|
| Quickstart | npx create-next-app@latest --example with-vitest |
| Install (TS) | Add vite-tsconfig-paths for path-alias resolution |
| Install (JS) | Skip vite-tsconfig-paths — no tsconfig.json to read |
| DOM environment | environment: 'jsdom' — Vitest defaults to node, which has no DOM |
| Default run mode | Watches continuously — use vitest run for a single pass |
| Async Server Components | Not supported — use E2E testing instead |
| Testing API | Nearly identical to Jest — render, screen.getByRole, etc. |
Vitest's real pitch relative to Jest isn't a different testing philosophy — the actual test-writing experience is close to identical — it's underlying execution speed, particularly valuable as a suite grows into the hundreds or thousands of tests. If you're starting a genuinely new project with no existing Jest investment to preserve, and raw test-run speed matters to your workflow, Vitest is a reasonable default; if you're already deep in a Jest-based suite, the migration cost of switching runners is real and worth weighing against the speed gain before committing to it.


