In Blog #31, we set up Vitest and React Testing Library for unit and component tests. In Blog #32, we added Playwright for complete browser workflows. Now we combine those tools into a practical Next.js component testing strategy for the three most important App Router building blocks: Server Components, Client Components and Server Actions.
These building blocks do not share one runtime or one risk profile. A Server Component may resolve data before HTML reaches the browser. A Client Component owns state, effects and event handlers. A Server Action crosses back to trusted server code to validate input, authorize a user, write data and refresh the interface. No single test type is best for all of them.
How Should You Test Server Components, Client Components and Server Actions in Next.js?
Use Vitest for pure logic and small synchronous units, React Testing Library for Client Component behavior, and Playwright for complete browser flows including async Server Components and Server Action-driven mutations. Extract reusable validation or business logic where useful, but keep framework behavior tested at the level where users actually experience it.
Testing Strategy at a Glance
Begin with the lowest-cost test that can produce reliable evidence. Move outward when behavior depends on request context, rendering, navigation, cache invalidation or persistence. “Lowest” does not mean “always unit.” For an async page whose contract is visible HTML, a small Playwright check may be simpler and more honest than a complex imitation of the server runtime.
Full flow: Playwright
| Code or behavior | Best starting test |
|---|---|
| Pure validation helper | Vitest |
| Price calculation | Vitest |
| Client button behavior | Vitest + RTL |
| Client form interaction | Vitest + RTL |
| Async Server Component rendering | Playwright |
| Server Action validation helper | Vitest |
| Full Server Action submission | Playwright |
| Redirect after mutation | Playwright |
| Cache revalidation visible to a user | Playwright or integration test |
| Authorization rule | Unit/integration plus E2E for a critical path |
Why App Router Testing Is Different
Classic client-heavy React puts much of the application logic inside a browser component. App Router splits work across server and client boundaries. Server Components render on the server and can access server-side data. Client Components add interactivity. Server Actions are server functions invoked from forms or client code. Testing should follow those execution boundaries rather than pretending everything is a jsdom component.
Server Components vs Client Components vs Server Actions
Server Component
- Runs on the server
- Can fetch data
- No browser event handlers
Client Component
- Interacts in the browser
- Owns state and effects
- Handles user events
Server Action
- Runs a server mutation
- Validates and authorizes
- Writes, redirects or revalidates
Choose the Right Test Level
Unit tests
Unit tests are ideal for deterministic calculations, parsers, validation rules and permission decisions. They should be fast, explicit and independent of the framework runtime. A unit test cannot establish that a browser form sent the expected request or that a revalidated page is fresh.
Component tests
Component tests render a small React UI in a DOM-like environment. They are strongest for Client Component props, state, conditional content, accessibility and interaction. jsdom is not a full browser and does not recreate the React Server Components protocol.
Integration tests
Integration tests connect meaningful layers: a service to a disposable database, a schema to a use case, or an authorization policy to a repository. Select boundaries based on risk. An integration suite that secretly connects to production is a dangerous deployment script, not a test.
E2E tests
End-to-end tests run a real browser against a running application. They cover routing, server rendering, JavaScript hydration, Server Action submission, redirect behavior and the final UI. They cost more to run, so reserve them for contracts whose value depends on those integrated layers.
Testing Pure Business Logic
Extract logic when it represents a reusable business rule—not merely to make a line testable. A calculation without request, database or React dependencies becomes a stable unit:
// calculate-total.ts
export function calculateTotal(price: number, quantity: number) {
return price * quantity
}
// calculate-total.test.ts
import { describe, expect, it } from 'vitest'
import { calculateTotal } from './calculate-total'
describe('calculateTotal', () => {
it('calculates the total', () => {
expect(calculateTotal(25, 2)).toBe(50)
})
})This test is fast, deterministic and framework-independent. Add edge cases required by the domain, such as negative quantities or currency rounding, rather than chasing lines. Keep orchestration in the action when extraction would create a fragmented architecture with no reuse or clearer contract.
Testing Synchronous Server Components
A synchronous, dependency-light Server Component can be treated like rendered React output when the installed setup supports it. Current Next.js Vitest guidance demonstrates unit testing a simple page component, but that does not mean every server dependency works inside jsdom. Keep the example honest:
type GreetingProps = { name: string }
export default function Greeting({ name }: GreetingProps) {
return <h1>Hello {name}</h1>
}
it('renders the supplied name', () => {
render(<Greeting name="Ada" />)
expect(screen.getByRole('heading', { name: 'Hello Ada' })).toBeVisible()
})If the component imports request-only APIs, a server-only SDK or a database client, do not keep expanding mocks until it resembles production. Test pure collaborators and move the integrated rendering assertion to an appropriate runtime.
Testing Async Server Components
An async Server Component may fetch data, query a database, read request context, check authentication and render server-derived output. Current Next.js guidance says some tools do not fully support async Server Components and recommends E2E testing over unit testing for them. That limitation is important: do not call an async component as if it were an ordinary browser component and present a passing artificial test as framework confidence.
A browser test should use a real route from the application, not a route copied from a tutorial. For example, if the audited app actually has /products and a Products heading:
test('server-rendered products are visible', async ({ page }) => {
await page.goto('/products')
await expect(page.getByRole('heading', { name: /products/i }))
.toBeVisible()
})Prefer deterministic seed data or a controlled service in the test environment. A production catalogue that changes during the run makes the assertion ambiguous.

Testing Client Components
Client Components are natural React Testing Library subjects when they manage toggles, dialogs, tabs, local forms, state, effects or browser events. Test through roles, labels and visible outcomes. Avoid asserting a hook’s internal value, implementation-specific class or private callback sequence.
'use client'
import { useState } from 'react'
export function ToggleDetails() {
const [open, setOpen] = useState(false)
return <>
<button onClick={() => setOpen(!open)}>Toggle details</button>
{open && <p>More information</p>}
</>
}const user = userEvent.setup()
render(<ToggleDetails />)
await user.click(
screen.getByRole('button', { name: /toggle details/i })
)
expect(screen.getByText('More information')).toBeVisible()userEvent.setup() is the current recommended style for user-event sessions. Await interactions because they can trigger asynchronous browser-like behavior. Query a control the way a user or assistive technology discovers it.
Testing Forms, useActionState and useFormStatus
Forms need two complementary test levels. At component level, verify fields, labels, local hints, disabled controls and client-owned validation. At E2E level, verify submission, server validation, mutation, navigation and refreshed output. See Blog #18 for the full forms and validation architecture.
For useActionState, test a separated schema or rule directly, cover pending and error rendering where the component boundary is practical, then exercise the real action flow in a browser. Do not invent an action signature for the installed React version—inspect the project first. For useFormStatus, place the submit control within its parent form and test what the user sees while submission is pending.
Pending UI can be too brief to observe reliably in a fast local environment. Control the boundary at component level or use a purpose-built test service delay—not an arbitrary sleep in the assertion. The production interface should remain usable if JavaScript loads slowly or progressive enhancement is part of the design.
Testing Server Actions
A Server Action can parse FormData, validate input, authenticate the session, authorize the operation, call a service, mutate the database, invalidate cached data, redirect and translate expected failures into safe UI. A single giant mocked unit test is a poor model of that sequence. Split confidence by responsibility while keeping the production action readable.
A generic shape can look like this, but adapt it to the audited application rather than creating fake production code for an article:
'use server'
export async function createTodo(formData: FormData) {
const parsed = todoSchema.safeParse({
title: String(formData.get('title') ?? ''),
})
if (!parsed.success) return { error: 'Invalid title' }
await requireTodoWritePermission()
await saveTodo(parsed.data)
revalidatePath('/todos')
}
Extracting and Testing Validation Logic
Use the same validation schema in production and tests; never copy it into the test. If the real application uses Zod, Valibot, Yup or a custom validator, preserve that choice. This publishing repository contains none of those libraries, so the following Zod example is illustrative:
describe('todoSchema', () => {
it('rejects an empty title', () => {
const result = todoSchema.safeParse({ title: '' })
expect(result.success).toBe(false)
})
it('accepts a meaningful title', () => {
const result = todoSchema.safeParse({ title: 'Review the test plan' })
expect(result.success).toBe(true)
})
})Cover boundaries and normalized values that matter to the domain. A client-side required attribute improves feedback but is not a security boundary. Repeat validation on the server because callers can invoke an endpoint without your UI.
Testing Authentication and Authorization
Authentication answers “Who are you?” Authorization answers “Are you allowed to do this?” A valid session does not imply permission to edit every record. Server Actions must enforce both on the server. Revisit Blog #13 for authentication architecture and Blog #27 for cross-site request protections; the dedicated authentication testing guide remains planned.
Unit-test pure policy helpers with owners, roles and resource attributes. Integration-test the real protected boundary where the environment supports it. For a sensitive action, keep an E2E allowed case and denied case. The denied test should confirm no mutation occurred and that the response reveals no sensitive record details.
Testing Database Mutations Safely
Never use the production database. Choose a disposable test database, transaction rollback, container, temporary schema or per-run namespace according to the actual architecture. Transaction rollback may be convenient for service integration tests, while an E2E flow spans multiple requests and often needs owned records plus explicit cleanup.
Never: Tests → Production database
Seed the minimum deterministic state for each case. Use unique identifiers when tests run concurrently. Assert durable state when persistence is central: after submission, reload or revisit the page and verify the record still appears. Keep email, payments, storage and other side effects on vendor sandboxes or controlled adapters.
Testing redirect()
A mock can prove that one function was called with one string, but it does not prove that a user reached a usable destination. In a Playwright test, submit the form, assert toHaveURL(), then assert a distinctive accessible heading or message on the destination. Because redirect() interrupts control flow, keep it outside broad try/catch blocks that would swallow the redirect signal.
await page.getByRole('button', { name: /save/i }).click()
await expect(page).toHaveURL(/\/dashboard/)
await expect(page.getByRole('heading', { name: /dashboard/i }))
.toBeVisible()Testing revalidatePath() and revalidateTag()
Cache APIs are not merely function calls; their contract is fresh user-visible data. After a mutation, verify that the affected page or component shows the new state. When the application uses revalidateTag, current Next.js documentation requires a cache-life profile for stale-while-revalidate behavior; updateTag is the Server Action-oriented option for read-your-own-writes semantics. Inspect the installed Next.js version and existing cache strategy before choosing an assertion.
For eventual stale-while-revalidate behavior, assert according to the product contract rather than requiring immediate replacement. For an action that must display its write immediately, choose the application API and cache design that provides that guarantee. Blog #9 covers the broader caching and revalidation model.
Error, Loading and Optimistic States
Cover expected validation errors, authorization failures and safe domain failures. Component tests can verify visible messages and accessible associations. E2E tests can confirm the complete page remains useful and does not expose stack traces, SQL messages or secrets. If error.tsx exists, exercise a controlled test-only failure or an expected application boundary rather than destabilizing production.
Test loading indicators only when they are a real user contract. Prefer deterministic controls over fixed sleeps. If the UI uses optimistic state, assert the immediate optimistic change, eventual server-confirmed state, and rollback or error state where supported.
Testing Server Action Forms and Server Data with Playwright
Start from user intent: navigate to the real page, fill labeled fields, submit through the visible control, and assert an outcome. Avoid calling action internals from the browser test. A critical mutation test should prove that validation, auth, persistence, redirect or refresh, and rendering agree.
test('creates a todo through the real form', async ({ page }) => {
await page.goto('/todos')
await page.getByLabel('Title').fill('Review test boundaries')
await page.getByRole('button', { name: /add todo/i }).click()
await expect(page.getByText('Review test boundaries')).toBeVisible()
await page.reload()
await expect(page.getByText('Review test boundaries')).toBeVisible()
})Only use this route and wording if the audited application contains them. Use fixtures to create a unique record, authenticate through a safe test identity, and clean up through a controlled test API or database owner. Do not make the spec dependent on execution order.
Mocking Boundaries: What to Mock and What Not to Mock
Mock slow or unsafe boundaries in focused tests: a payment adapter, email sender, clock, random identifier or repository contract. Do not mock every collaborator. A test in which routing, request context, database, cache and rendering are all fake can pass while the product is broken.
Prefer dependency injection at a stable application boundary over patching deep Next.js internals. When a test must stub fetch, verify the meaningful request and response behavior, reset the stub after each case, and retain a contract or integration test for the remote shape.
Test Isolation and Test Data
Each test should own the data it creates. Generate a per-run identifier, create only required records and remove them even after failure. Do not share one mutable account across parallel workers. Freeze time only through an intentional seam and reset it after the case. Define environment guards so a destructive suite refuses to run when a production hostname, database or credential is detected.
Authentication storage state can contain live cookies and tokens. Keep it outside source control, scope it to a disposable account and expire it quickly. Browser traces and screenshots may capture personal data; limit collection and retention, and sanitize values before they reach CI artifacts.
Secure Server Action Pipeline
Server Actions are public server entry points in the security sense: never assume that only your React form can invoke them. Parse untrusted data, validate on the server, establish identity, check resource-level permission, perform the mutation, and return only safe expected errors. Apply rate limits or idempotency where the action risk calls for them.
Build an App Router Test Matrix from the Real Repository
Before creating tests, inventory code by execution boundary. Search the application for 'use client', 'use server', action=, useActionState, useFormStatus, redirect, revalidatePath, revalidateTag, cookies, headers, route handlers, validation schemas, database calls and authentication checks. The search result is not the test plan; it is the evidence used to build one.
Classify every high-risk target as pure logic, synchronous Server Component, async Server Component, Client Component, Server Action, Route Handler or integration layer. Then write down the observable contract, failure impact, environment dependency and cheapest reliable test. A pricing helper may need dozens of fast boundary cases. A read-only async page may need one browser assertion plus service-level coverage. A privileged delete action may justify policy unit tests, repository integration tests and two carefully isolated E2E journeys.
| Audited target | Primary evidence | Important negative case |
|---|---|---|
| Pure utility | Returned value in Vitest | Boundary or malformed input |
| Validation schema | Parsed normalized data | Missing, oversized or hostile input |
| Synchronous Server Component | Accessible rendered output | Empty or alternate props |
| Async Server Component | Browser-visible server data | Empty, denied or safe error state |
| Client Component | Interaction and visible UI | Keyboard, invalid input or failed request |
| Server Action | Mutation and final UI | Invalid, unauthenticated and unauthorized requests |
| Repository/service | Isolated integration result | Conflict, missing record or dependency failure |
Prioritize by risk instead of file count. Money, permissions, account ownership, destructive operations and compliance-sensitive data deserve deeper coverage than a decorative toggle. Record what is intentionally not covered and why. This makes the matrix a review tool rather than an automatic promise that every row needs every kind of test.
When a Direct Server Action Test Helps—and When It Does Not
Calling a Server Action function directly can be useful when its inputs and collaborators are controlled and the goal is to verify orchestration. For example, a focused test might supply FormData, stub one repository boundary, and assert the safe returned validation state. That still runs outside a real browser submission, request lifecycle and cache refresh. Label it accurately as a unit or focused integration test, not end-to-end coverage.
Direct tests become brittle when they reproduce framework internals. Mocking cookies(), headers(), navigation exceptions, cache stores and React form state in the same test creates a private version of Next.js that your application must maintain. If the action mostly coordinates those APIs, a Playwright flow often supplies clearer evidence with less custom simulation.
A balanced action suite asks distinct questions:
- Does the schema accept and normalize valid input?
- Does the permission rule deny the wrong owner or role?
- Does the repository persist the intended shape in an isolated database?
- Does the action return a safe expected error without leaking implementation details?
- Can the real form submit successfully through a browser?
- Does the final URL and rendered data reflect the mutation?
Do not assert every intermediate function call merely because a mock exposes it. Assertions should protect durable contracts. An implementation may replace revalidatePath with a tag strategy while preserving the same fresh UI; a user-oriented assertion should continue to pass.
Design Negative Cases Before the Happy Path Is Finished
Failures reveal whether boundaries are real. For validation, cover empty values, type coercion, length limits and domain-specific constraints. For authentication, cover the absence of a session. For authorization, authenticate a user who does not own the resource. For concurrency, consider a stale version or duplicate submission. For services, reproduce an expected timeout or domain failure through a controlled adapter.
Every denied mutation should prove two things: the user receives a safe response and protected state remains unchanged. A visible “not allowed” message alone is insufficient if the database write already occurred. Conversely, inspecting only the database misses whether the user is trapped on a broken page. Combine the cheapest state assertion with the appropriate visible assertion.
Server validation messages should not echo raw database errors, stack traces, tokens or rejected secrets. Associate field errors with their controls and expose summary messages through an appropriate live region. In Playwright, locate the error by role or associated text rather than a red CSS class. This protects accessibility and behavior at the same time.
Retries deserve special care. A user may double-click, a mobile connection may retry, or a browser may resubmit after navigation. High-impact actions can need idempotency or conflict handling. Test the business guarantee—one charge, one invitation or one state transition—rather than assuming the UI prevents every duplicate request.
Keep Component and Action Tests Maintainable
Place a test near its source when that matches repository convention, or use an established test directory. Do not reorganize the application solely to imitate a tutorial tree. Name cases by behavior: “denies a non-owner” communicates more than “calls authorize.” Keep fixture builders small and explicit so a reader can see which values matter.
Prefer factories that return valid defaults with deliberate overrides. A fixture containing every production column becomes hard to update and can hide the condition under test. Avoid global mutable fixtures. Create data per case or per worker, and make cleanup resilient when an assertion fails midway.
Semantic queries are part of the application contract. Use roles, labels and accessible names for controls. Use visible text when it is stable and meaningful. Reach for a test ID when no semantic query represents the element, and name it after behavior rather than layout. Avoid CSS paths, generated class names and repeated nth() selectors.
Keep each browser test focused on one meaningful journey, but do not split a single contract into so many specs that setup dominates. Use Playwright’s web-first assertions and automatic waiting. Wait for an observable state—URL, heading, response-backed content, enabled control—not a fixed number of milliseconds.
When a failure occurs, classify it as product regression, test defect, environment problem or external dependency outage. Preserve bounded traces and screenshots for investigation, remembering that artifacts can contain form values and cookies. A flaky test is information about synchronization, isolation or product instability; repeated retries are not a permanent fix.
Review the Boundary, Not Just the Test File
A test review should inspect the production boundary beside the spec. For a Client Component, confirm that server-only code does not leak into the client bundle and that serialized props contain no secret. For a Server Component, check whether the data query is bounded, authorization occurs before sensitive data is shaped, and empty states are deliberate. For a Server Action, review validation, resource-level authorization, mutation scope, error translation and cache behavior together.
Ask what a passing test actually proves. A mocked repository proves the action called a contract, not that the schema matches the database. A database integration test proves persistence, not that the form is labeled or the page refreshes. A browser test proves the configured journey, not every policy edge case. Write the missing evidence in the matrix instead of stretching one test beyond its meaning.
Also ask what can safely run in parallel. Pure tests normally can. Database tests need unique records, transactions or worker namespaces. Browser tests need independent accounts and URLs. If a suite requires serial execution, document the real shared resource and plan to remove it; serial ordering can hide state leaks and make CI much slower.
Finally, treat version changes as architecture changes. Read release notes and current official documentation before upgrading Next.js, React, Vitest, Testing Library or Playwright. Re-run the focused suite, production build and browser journeys. Pay special attention to cache semantics, Server Action behavior, React form hooks and test-environment support rather than assuming that an old workaround is still required.
Production Testing Strategy
Keep most checks close to the code and a smaller number close to user reality. Unit-test domain rules and security policies. Component-test interactive UI. Integration-test persistence and service contracts in disposable infrastructure. E2E-test the paths where a server-rendered page, browser interaction and server mutation must cooperate.
Do not run destructive tests against production. Production smoke checks should be read-only unless a rigorously isolated synthetic tenant and cleanup process exist. Monitor real errors and performance after deployment because pre-release tests cannot reproduce every network, browser and data condition.
Common Next.js App Router Testing Mistakes
- Trying to unit-test every integrated behavior.
- Treating async Server Components like Client Components.
- Testing component internals instead of visible output.
- Reading Client Component state instead of user behavior.
- Putting validation, permission and persistence into one untestable action.
- Mocking every dependency or deep Next.js internal.
- Using a production database or production Server Action.
- Testing
revalidatePathonly as a mocked call. - Testing redirect only as an implementation detail.
- Trusting client-side validation or ignoring authorization.
- Sharing mutable data or depending on test order.
- Using fixed sleeps in E2E tests.
- Skipping invalid, denied and service-failure cases.
- Overusing snapshots for interactive behavior.
- Restructuring the whole application only for testability.
Next.js 16 App Router Testing Best Practices
- Match the test type to the execution boundary.
- Unit-test pure validation and business logic.
- Component-test Client Component behavior with semantic queries.
- Use Playwright for async Server Component output and critical action flows.
- Keep server-side authentication and authorization intact.
- Test success, validation, denied and expected failure states.
- Use isolated test services and owned data.
- Avoid excessive framework mocks and arbitrary sleeps.
- Test redirects from the user’s perspective.
- Verify revalidated data is actually visible.
- Keep cases independent and concurrency-safe.
- Run lint, types, Vitest, build and a deliberate E2E suite in CI.
- Review current Next.js and testing-tool documentation when versions change.
Testing & Quality Hub
#31 Vitest & React Testing LibraryPublished
#32 Playwright E2E TestingPublished
#33 Server Components, Client Components & Server Actions TestingPublished
#34 Route Handlers & API TestingPublished
#35 Authentication TestingPlanned
#36 Database TestingPlanned
#37 Mocking APIs & External ServicesPlanned
#38 Playwright Authentication & User FlowsPlanned
#39 Testing with GitHub ActionsPlanned
#40 Production Testing StrategyPlanned
Frequently Asked Questions
How do I test Server Components in Next.js?
Test synchronous, dependency-light Server Components with Vitest when practical. For async Server Components, test pure helpers separately and verify rendered behavior with Playwright.
Can Vitest test Server Components?
Vitest can cover synchronous Server Components and pure server-side logic. Compatibility still depends on the component dependencies and test environment.
Can Vitest test async Server Components?
Current Next.js guidance recommends end-to-end testing over unit testing for async Server Components because some tools do not fully support them.
How do I test Client Components?
Render them with React Testing Library, interact through user-event, and assert accessible, visible outcomes rather than internal state.
How do I test useState behavior?
Trigger the control a user would operate and assert the resulting UI. Do not read or mutate a component state variable directly.
How do I test Server Actions?
Unit-test extracted validation and business rules, integration-test safe service or repository boundaries, and use Playwright for the real form-to-action-to-UI flow.
Should I call Server Actions directly in unit tests?
Usually test their pure collaborators directly. A direct action test can be useful when the framework boundary is controlled, but it should not replace a browser-level workflow.
How do I test Server Action validation?
Export and test the same schema or pure validation helper used by the action, covering valid, invalid and boundary inputs without duplicating the rules.
How do I test Server Action redirects?
Submit through the browser, assert the final URL with Playwright, and verify meaningful destination content.
How do I test revalidatePath()?
Perform the safe mutation in an isolated environment and verify that the affected page displays fresh data. A mocked function call alone does not prove freshness.
How do I test revalidateTag()?
When the application uses tagged data, trigger the mutation and verify that a subsequent render exposes the updated user-visible result.
How do I test useActionState()?
Test separated validation and business rules, component-test feasible pending or error UI, and E2E-test the real action result.
How do I test useFormStatus()?
Submit the parent form and assert user-visible pending behavior such as a disabled button or loading label, then assert the completed state.
How do I test database mutations safely?
Use a disposable test database or namespace, deterministic seed data and cleanup. Never aim destructive tests at production.
Should I mock the database?
Mock a narrow repository boundary for focused unit tests, but retain integration coverage against an isolated database for important persistence behavior.
Should I mock Next.js APIs?
Only when a focused test needs a narrow boundary. Heavy mocking of redirects, request context and caching often creates brittle tests with weak confidence.
When should I use Playwright instead of Vitest?
Use Playwright for navigation, async Server Component output, real Server Action submissions, redirects, revalidation and critical browser workflows.
How do I test authorization in a Server Action?
Unit-test pure permission rules, integration-test the protected boundary where useful, and E2E-test critical allowed and denied journeys.
How do I avoid flaky App Router tests?
Isolate data, avoid execution-order dependencies, use semantic locators and web-first assertions, and never replace readiness checks with fixed sleeps.
What should run in CI?
A typical pipeline runs lint, types, Vitest, a production build and a risk-based Playwright suite using the project package manager and isolated services.
Current Official References
- Next.js testing guide
- Next.js Vitest guide
- Next.js Playwright guide
- Server and Client Components
- Updating data and Server Actions
- redirect API
- revalidatePath API
- revalidateTag API
- Vitest guide
- Testing Library user-event
- Playwright documentation
Next Steps
Audit the real application first, then create a small test matrix for its pure utilities, validation helpers, synchronous and async Server Components, Client Components, Server Actions, redirects, revalidation and authorization boundaries. Continue using Blog #31 for focused tests and Blog #32 for browser setup. For connected architecture, revisit Blog #7 on Server Actions, Blog #10 on Route Handlers, Blog #13 on authentication, Blog #18 on forms and Blog #27 on CSRF protection.
