Skip to content

Next.js Pages Router - Getting Started

Test your Next.js Pages Router application with API routes, getServerSideProps, and getStaticProps all executing. No mocking of Next.js internals required.

Terminal window
npm install @scenarist/nextjs-adapter
npm install -D @playwright/test @scenarist/playwright-helpers
lib/scenarios.ts
import type { ScenaristScenario, ScenaristScenarios } from '@scenarist/nextjs-adapter/pages';
// ✅ RECOMMENDED - Default scenario with complete happy path
const defaultScenario: ScenaristScenario = {
id: 'default',
name: 'Happy Path',
description: 'All external APIs succeed with valid responses',
mocks: [
// Stripe: Successful payment
{
method: 'POST',
url: 'https://api.stripe.com/v1/charges',
response: {
status: 200,
body: { id: 'ch_123', status: 'succeeded', amount: 5000 },
},
},
// Auth0: Authenticated standard user
{
method: 'GET',
url: 'https://api.auth0.com/userinfo',
response: {
status: 200,
body: { sub: 'user_123', email: 'john@example.com', tier: 'standard' },
},
},
// SendGrid: Email sent successfully
{
method: 'POST',
url: 'https://api.sendgrid.com/v3/mail/send',
response: {
status: 202,
body: { message_id: 'msg_123' },
},
},
],
};
// Specialized scenario: Override ONLY Auth0 for premium user
const premiumUserScenario: ScenaristScenario = {
id: 'premiumUser',
name: 'Premium User',
description: 'Premium tier user, everything else succeeds',
mocks: [
// Override: Auth0 returns premium tier
{
method: 'GET',
url: 'https://api.auth0.com/userinfo',
response: {
status: 200,
body: { sub: 'user_456', email: 'premium@example.com', tier: 'premium' },
},
},
// Stripe and SendGrid automatically fall back to default (happy path)
],
};
export const scenarios = {
default: defaultScenario,
premiumUser: premiumUserScenario,
} as const satisfies ScenaristScenarios;
lib/scenarist.ts
import { createScenarist } from '@scenarist/nextjs-adapter/pages';
import { scenarios } from './scenarios';
export const scenarist = createScenarist({
enabled: process.env.NODE_ENV === 'test',
scenarios,
});
pages/api/__scenario__.ts
import { scenarist } from '@/lib/scenarist';
export default scenarist.createScenarioEndpoint();
tests/fixtures.ts
import { withScenarios, expect } from '@scenarist/playwright-helpers';
import { scenarios } from '../lib/scenarios';
// Create type-safe test object with scenario IDs
export const test = withScenarios(scenarios);
export { expect };
tests/products.spec.ts
import { test, expect } from './fixtures'; // ✅ Import from fixtures
test('premium users see premium pricing', async ({ page, switchScenario }) => {
await switchScenario(page, 'premiumUser'); // ✅ Type-safe! Autocomplete works
await page.goto('/products');
// Your getServerSideProps runs, Auth0 returns premium tier
await expect(page.getByText('Premium Plan')).toBeVisible();
await expect(page.getByText('$50.00')).toBeVisible();
});
test('standard users see standard pricing', async ({ page, switchScenario }) => {
await switchScenario(page, 'default'); // Default scenario (happy path)
await page.goto('/products');
await expect(page.getByText('Standard Plan')).toBeVisible();
await expect(page.getByText('$25.00')).toBeVisible();
});

Example Page with getServerSideProps:

pages/products.tsx
export async function getServerSideProps() {
// This fetch is mocked by Scenarist
const response = await fetch('https://api.stripe.com/v1/products', {
headers: { 'Authorization': `Bearer ${process.env.STRIPE_KEY}` },
});
const { data: products } = await response.json();
return { props: { products } };
}
export default function ProductsPage({ products }) {
return (
<div>
{products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<span className="price">${(product.price / 100).toFixed(2)}</span>
</div>
))}
</div>
);
}
tests/checkout.spec.ts
test('processes payment via API route', async ({ page, switchScenario }) => {
await switchScenario(page, 'success');
// Your API route validation runs
const response = await page.request.post('/api/checkout', {
data: { amount: 5000, token: 'tok_test' },
});
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.status).toBe('succeeded');
});

Example API Route:

pages/api/checkout.ts
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// Validation runs normally
if (!req.body.amount || !req.body.token) {
return res.status(400).json({ error: 'Missing required fields' });
}
// External API call is mocked by Scenarist
const response = await fetch('https://api.stripe.com/v1/charges', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.STRIPE_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(req.body),
});
const data = await response.json();
res.status(200).json(data);
}

Why header forwarding matters: When your API routes or getServerSideProps call external APIs (that you’re mocking with Scenarist), you must forward the test ID header so MSW knows which scenario to use.

Use the getScenaristHeaders helper to safely extract Scenarist headers:

pages/api/products.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { getScenaristHeaders } from '@scenarist/nextjs-adapter/pages';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// Forward Scenarist headers to external API
const response = await fetch('https://api.stripe.com/v1/products', {
headers: {
...getScenaristHeaders(req),
'Authorization': `Bearer ${process.env.STRIPE_KEY}`,
},
});
const data = await response.json();
res.status(200).json(data);
}

The same helper works in getServerSideProps:

pages/products.tsx
import type { GetServerSideProps } from 'next';
import { getScenaristHeaders } from '@scenarist/nextjs-adapter/pages';
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const response = await fetch('https://api.stripe.com/v1/products', {
headers: {
...getScenaristHeaders(req),
'Authorization': `Bearer ${process.env.STRIPE_KEY}`,
},
});
const { data: products } = await response.json();
return { props: { products } };
};
export default function ProductsPage({ products }) {
return (
<div>
{products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<span className="price">${(product.price / 100).toFixed(2)}</span>
</div>
))}
</div>
);
}

The getScenaristHeaders helper extracts the test ID header (x-scenarist-test-id) for forwarding to external APIs.

API Routes Execute Normally - Your validation, error handling, and business logic all run as they would in production.

Server-Side Rendering Works - Your getServerSideProps and getStaticProps fetch data from mocked external APIs.

Test Isolation - Each test gets isolated scenario state. Run tests in parallel with zero interference.

No App Restart - Switch scenarios instantly during test execution.