57Next.js for SDET
What you will master here
- App Router vs Pages Router
- Server Components, Client Components, "use client"
- Data fetching: server fetch, ISR, SSG, SSR, dynamic
- Route handlers + Server Actions
- Middleware
- Testing approach: unit (Vitest) + integration + E2E (Playwright)
- SDET-specific gotchas (hydration, caching, streaming)
57.1 App Router vs Pages Router
| App Router (default since 13) | Pages Router (legacy) |
|---|---|
| File-based: app/users/[id]/page.tsx | File-based: pages/users/[id].tsx |
| Server Components by default | Client Components by default |
| Streaming + Suspense | SSR/SSG/ISR via getServerSideProps etc. |
| Route handlers (route.ts) | API routes (pages/api/*.ts) |
| Server Actions | — |
57.2 Server vs Client Components
// app/page.tsx — Server Component by default
async function Home() {
const users = await fetch('https://api/users').then(r => r.json());
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// app/Cart.tsx — Client Component (interactive)
'use client';
import { useState } from 'react';
export function Cart() {
const [items, setItems] = useState<Item[]>([]);
return <button onClick={() => setItems([...items, newItem])}>Add</button>;
}
Server Components never ship JS to the browser. They reduce bundle size dramatically. They can't use useState, useEffect, browser-only APIs.
57.3 Data fetching modes
| Mode | When | How |
|---|---|---|
| Static (SSG) | Build-time HTML, fastest | Default if no dynamic data |
| ISR | Static but revalidate every N seconds | fetch(url, { next: { revalidate: 60 } }) |
| SSR | Per-request server render | fetch(url, { cache: 'no-store' }) or dynamic = 'force-dynamic' |
| Client | Fetch in useEffect / SWR / TanStack Query | useEffect / useSWR |
57.4 Route handlers
// app/api/users/route.ts
export async function GET() {
const users = await db.user.findMany();
return Response.json(users);
}
export async function POST(req: Request) {
const body = await req.json();
const u = await db.user.create({ data: body });
return Response.json(u, { status: 201 });
}
57.5 Server Actions
// app/actions.ts
'use server';
export async function createOrder(formData: FormData) {
const sku = formData.get('sku') as string;
const order = await db.order.create({ data: { sku }});
return order.id;
}
// app/CartForm.tsx
import { createOrder } from './actions';
export default function CartForm() {
return (
<form action={createOrder}>
<input name="sku" />
<button>Buy</button>
</form>
);
}
Server Actions are RPC-like functions you can call from client components or use as form actions. No need to write a route + client fetch.
57.6 Middleware
// middleware.ts
import { NextResponse } from 'next/server';
export function middleware(req) {
if (!req.cookies.get('token')) {
return NextResponse.redirect(new URL('/login', req.url));
}
}
export const config = { matcher: ['/dashboard/:path*'] };
57.7 Testing Next.js apps
| Layer | Tool |
|---|---|
| Pure logic (utils, hooks) | Vitest |
| UI components | Vitest + RTL OR Playwright CT |
| Route handlers / API | Vitest hitting the function, or Playwright request to dev server |
| Server Actions | Integration: spin Next dev server, call action via form submission |
| E2E (full app) | Playwright with the dev server running |
// playwright.config.ts — boot Next in CI
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
57.8 SDET gotchas
- Hydration mismatch — server HTML differs from first client render. Common cause:
new Date(), locale-dependent formatting,windowaccess during render. - Cache surprises — fetch is cached by default in Server Components. Forgetting
cache: 'no-store'serves stale data. - Streaming — Suspense + streaming means parts of the page arrive late. Tests should wait for specific text, not full load.
- "use client" propagation — once you mark a component, its children must also be client-compatible. SDETs check that server-only modules aren't imported into client bundles (leaks secrets).
- Route segment config —
export const dynamic,revalidate,runtimeper route. Wrong choice → unexpected SSR behaviour.
Module 61 — Next.js Q&A
Server vs Client Components — fundamental difference?
Server Components render on the server only — no JS shipped to browser. They can read databases/files directly. Client Components render on both (initial SSR + hydration on client) and can use state/effects/browser APIs. Mark with
'use client' at top of file.SSG vs SSR vs ISR?
SSG: build-time static, fastest. SSR: per-request server render, fresh but slower. ISR: static cached, revalidates every N seconds (best of both). Set per fetch or per route segment.
What's a Server Action?
A function marked
'use server' that runs on the server. Callable from client components or HTML forms. Removes the need for a separate API route + client fetch.How do you handle auth in Next.js middleware?
Edge middleware in middleware.ts inspects the request, checks a cookie/header, and either continues, rewrites, or redirects. Runs before any page/route handler. Use a matcher to limit which paths it applies to.
What's a hydration mismatch?
Server HTML differs from the client's first render. React logs a warning, may re-render the affected subtree (visible flicker). Causes:
new Date(), random ids, browser-only APIs in render, timezone differences.How do you E2E-test a Next.js app?
Playwright with webServer config booting
next start (or next dev locally). Visit pages, assert by role/text. For data-dependent tests, seed via Server Action or API route.What's the App Router fetch cache behaviour?
By default, fetch in Server Components is cached forever (treated like SSG). Add
{ cache: 'no-store' } for dynamic, or { next: { revalidate: 60 } } for ISR. Forgetting → stale data bug.How do you stream a slow part of the page?
Wrap a slow component in
<Suspense fallback={...}>. Next streams the shell + fallback immediately, then sends the real content when ready. Tests should wait on the loaded state, not initial.