Test and improve application performance
Set Up Vitest in a Next.js Pages Router App—and Know Its Testing Boundary
Set up Vitest, jsdom, and React Testing Library for a Next.js Pages Router page, and understand what the first test does not check.

For a Pages Router app, a useful first Vitest test is small: render the component exported by pages/index.tsx and find its heading by accessible role and name. That checks the component’s rendered output. It does not, by itself, check what happens when someone requests or navigates to the route, which route-level HTTP and browser checks can cover.
This walkthrough follows the Next.js Pages Router Vitest setup. It assumes an existing TypeScript Next.js project with a pages directory. The setup guidance does not pin package versions, so treat this as a configuration to apply to your project, not a promise about every combination of Next.js and Vitest releases.
Install the test dependencies
From the project root, install Vitest, the Vite React plugin, jsdom, and React Testing Library’s packages. For TypeScript projects, the documented package set also includes vite-tsconfig-paths:
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom vite-tsconfig-paths
The path plugin is relevant when tests need aliases defined in your TypeScript configuration. The first test below uses a relative import, so that particular import does not depend on an alias. Including the plugin keeps the configuration ready for tests that do use those paths.
Configure Vitest at the project root
Create vitest.config.ts alongside your project’s package.json:
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',
},
})
This connects the documented React and TypeScript path plugins to Vitest and selects jsdom as the test environment. The setting is for the test run: it does not replace your Next.js configuration or start a Next.js server. That distinction matters when you interpret the result later.
Add test inside the existing scripts object in package.json. For example, if your scripts contain the usual development, build, and start entries, the complete object can look like this:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"test": "vitest"
}
}
Keep any other scripts your project already needs; the addition here is "test": "vitest". The Pages Router setup guide notes that running this script watches for project changes by default. You do not need a separate watch script for this first local test.
Render a Pages Router page
Use a page with one unambiguous heading. Put this component in pages/index.tsx:
export default function Page() {
return (
<main>
<h1>Home</h1>
</main>
)
}
Then create __tests__/home.test.tsx at the project root:
import { expect, test } from 'vitest'
import { render, screen } from '@testing-library/react'
import Page from '../pages/index'
test('the home page renders its heading', () => {
render(<Page />)
expect(screen.getByRole('heading', { level: 1, name: 'Home' })).toBeDefined()
})
The relative import works because __tests__ and pages sit beside each other at the project root. The __tests__ directory is a convention, not a requirement for the assertion: if you put the test elsewhere, adjust its import path to match. Notice also that the test imports the page component directly. It does not ask Next.js to resolve a route before rendering it.
Run the script from the project root:
npm run test
For this example, the success check is a passing assertion for a level-one heading named Home; Vitest then remains in its documented watch workflow. The Pages Router example uses the same render-and-heading query. Querying by role, level, and accessible name states precisely which heading the test expects, without tying the assertion to the <main> wrapper.
Keep the test boundary in view
The test imports Page and calls render(<Page />). Its claim is narrow: the imported component renders a matching heading in the test DOM. It does not visit a URL, check whether a browser can request the route, or establish that navigation through the running Next.js app works. If that is the behavior you need confidence in, a passing component assertion is not an answer to that question.
Separate App Router guidance: The Next.js 14 App Router Vitest guide says Vitest does not support async Server Components in that guide’s context. It says synchronous Server and Client Components can be unit tested and recommends end-to-end tests for async components. Use the render test here for a component-level assertion; for an async App Router Server Component, follow the guidance applicable to your Next.js version rather than treating this Pages Router test as an end-to-end check.




