In Blog #33, we built a testing strategy for Server Components, Client Components and Server Actions. The next boundary is the HTTP layer itself. Route Handlers receive requests from browsers, mobile clients, webhooks and external services, so they need tests for status codes, validation, authentication, authorization, data behavior and failure cases.
Next.js API testing should answer more than “does this function run?” A useful suite proves that an endpoint accepts the intended method and input, returns a stable response contract, protects identity and ownership boundaries, changes the correct state, and fails without leaking sensitive internals.
How Do You Test Next.js Route Handlers?
Test pure validation and business logic with Vitest, test Route Handler behavior at the request/response boundary with integration tests, and use Playwright API or browser testing for real HTTP flows. Verify status codes, response bodies, headers, authentication, authorization, database changes and failure cases without calling production services.
API Testing at a Glance
Use the smallest test that provides credible evidence. Pure functions need no HTTP object. A handler contract needs a Request and Response. Critical routing, cookies and deployment behavior need a running application. Combining these layers produces faster feedback without pretending a mock recreates the entire framework.

| Test type | Purpose | Example |
|---|---|---|
| Unit | Pure validation and business rules | Normalize an email or validate a payload |
| Integration | Route plus service or database boundary | Call an exported POST with a controlled repository |
| API E2E | Real HTTP endpoint | Playwright request to the running server |
| Browser E2E | User flow that invokes the endpoint | Form → API → success or error UI |
What Is a Route Handler?
In the App Router, a Route Handler is a route.ts or route.js file inside app. It uses the Web Request and Response APIs and may opt into Next.js helpers through NextRequest and NextResponse. Current documentation lists GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS; unsupported methods receive 405.
app/api/example/route.ts
export async function GET() {
return Response.json({ ok: true })
}
export async function POST(request: Request) {
const body = await request.json()
return Response.json({ received: body }, { status: 201 })
}Route Handlers vs Server Actions vs pages/api
Blog #10 explains Route Handler implementation, while Blog #7 covers Server Actions. Route Handlers are explicit HTTP endpoints and can serve external clients. Server Actions are designed primarily for mutations from a Next.js UI. The older pages/api convention belongs to the Pages Router; do not paste its request and response types into an App Router test.
| Concern | Route Handler | Server Action |
|---|---|---|
| HTTP endpoint | Yes | Not primarily |
| External clients | Yes | Usually no |
| Browser form mutation | Possible | Excellent fit |
| Explicit HTTP status | Yes | Different response model |
| Webhooks | Yes | No |
| Internal UI mutation | Possible | Often preferable |
Route Handler Anatomy and Contract
Treat the method, path, accepted input, authentication expectation, status codes, headers and response schema as one contract. Tests should protect that contract rather than freeze private helper calls. Standard Request is enough for many handlers; use NextRequest when the route needs helpers such as nextUrl or request cookies.
Unit Testing API Logic with Vitest
Pure logic should not require a fabricated request. Test normalization, calculations, permission decisions and schemas as plain functions. This keeps failures narrow and fast.
import { describe, expect, it } from 'vitest'
import { normalizeEmail } from './normalize-email'
describe('normalizeEmail', () => {
it('trims and lowercases an address', () => {
expect(normalizeEmail(' Ada@Example.COM '))
.toBe('ada@example.com')
})
})If the application uses Zod or another schema library, import the production schema. Blog #18 explains shared form and validation boundaries. Cover a valid payload, each required field, invalid types, formats and relevant boundaries. A test-only copy of the schema can drift while every test remains green.
Integration Testing Route Handlers Directly
When the handler and installed runtime support it, construct a Web Request and invoke the exported method. This verifies parsing, status and body mapping while keeping the server out of the loop. It does not prove filesystem routing, middleware, deployment headers or the complete Next.js HTTP runtime.
import { expect, it } from 'vitest'
import { POST } from './route'
it('returns a created response', async () => {
const request = new Request('http://localhost/api/items', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: 'Example' }),
})
const response = await POST(request)
expect(response.status).toBe(201)
await expect(response.json()).resolves.toMatchObject({ ok: true })
})Testing GET, POST, PUT, PATCH and DELETE
GET Route Handlers
Cover success, empty collections, query parsing, missing resources and the controlled failures relevant to the implementation. A GET endpoint should not mutate application state. Current Next.js documentation says Route Handlers are not cached by default; GET can opt into caching, and Cache Components introduce additional documented behavior. Test the visible freshness contract instead of assuming old defaults.
POST Route Handlers and request bodies
Test valid JSON, malformed JSON, missing fields, domain validation, authentication, conflict behavior and service failure where those branches genuinely exist. request.json() can throw for malformed JSON, so the handler must deliberately translate that failure if its contract promises a clean client error.
PUT and PATCH
PUT conventionally represents replacement while PATCH represents partial change, but the application contract decides. For PATCH, prove omitted fields remain unchanged. For PUT, prove required replacement fields are enforced if that is the selected design.
DELETE
Never test destructive methods against production. Create a record owned by the test, delete it, verify the HTTP result and database state, then clean up safely when necessary.
Query Parameters, Dynamic Params, Headers and Cookies
For a query such as /api/products?search=keyboard&page=2, cover expected, missing, malformed and out-of-range values. Use new URL(request.url).searchParams with a standard Request, or request.nextUrl.searchParams when the route intentionally accepts NextRequest.
Dynamic parameters are version-sensitive. Current Next.js Route Handler documentation shows an async context and awaits ctx.params, with the typed RouteContext<'/users/[id]'> helper available after type generation. Verify this against the installed package rather than copying an older synchronous example.
Assert headers that belong to the public contract: content type, deliberate cache policy, CORS controls and required custom headers. Do not snapshot every framework-generated header. For cookies, test absent, valid, updated and cleared states that actually exist, and never print session values into test output or traces.
Status Codes, JSON and Safe Errors
| Status | Conventional meaning | Use only when |
|---|---|---|
| 200 | Successful response | The operation completed normally |
| 201 | Created | A new resource was created |
| 204 | No content | Success intentionally has no body |
| 400 | Bad request | Input cannot be parsed or accepted by contract |
| 401 | Unauthenticated | Acceptable credentials are absent |
| 403 | Forbidden | Identity exists but permission does not |
| 404 | Not found | The addressed resource is unavailable |
| 409 | Conflict | A real state or uniqueness conflict exists |
| 422 | Unprocessable content | The project adopts it for semantic validation |
| 429 | Too many requests | A rate limiter actually exists |
| 500 | Server error | An unexpected internal failure is safely translated |
Projects can choose different conventions; consistency and documentation matter more than forcing every code into every API. Prefer focused JSON assertions to giant snapshots. Verify stable fields and types, while allowing unrelated metadata to evolve.
Testing Authentication and Authorization
Blog #13 covers authentication architecture. At the route boundary, test no credential, a valid test identity and an invalid or expired credential only when the system can reproduce it safely. Then test authorization separately: an authenticated identity is not automatically allowed to access every object.
IDOR/BOLA defenses deserve an explicit ownership case: create a resource for test user A, authenticate as test user B, request A's protected resource, expect denial and prove the resource did not change. Do not weaken the test by mocking the permission decision to “allowed.”
Database Reads, Writes and Transactions
The database chapter will go deeper in Blog #36, which is not yet published. For Route Handler integration, use a disposable database, dedicated test schema, container or transaction strategy supported by the real stack. Seed a record before GET and assert the API representation. For POST, assert the response and query the test database to verify the correct row.
If production code uses a transaction, test meaningful all-or-nothing behavior: force a controlled second-step failure and prove the first write did not remain. Never introduce transaction claims for a stack that does not use them. Cleanup must target only data owned by the test, and mutation suites should fail closed when configuration resembles production.
External APIs, Mocking and Webhooks
Do not call an uncontrolled third party in every test. Put outbound work behind a thin service interface where that improves design, use vi.stubGlobal('fetch', vi.fn()) or the project-standard mock for focused cases, and restore globals after each test. Selected E2E checks can use a provider sandbox or controlled substitute. Blog #37 will cover this boundary in depth and remains intentionally non-clickable.
Webhook tests should use the provider's official signing tools with a test secret. Cover valid events, missing or invalid signatures, malformed bodies and unknown event types. Test duplicate delivery only when the system implements idempotency. Never disclose the secret or invent custom cryptography.
Playwright API Testing
Blog #32 establishes Playwright configuration. Its request fixture supplies an APIRequestContext that can call a server without loading a page. Use the project's safe baseURL and a non-production environment.
import { expect, test } from '@playwright/test'
test('GET API returns its public contract', async ({ request }) => {
const response = await request.get('/api/example')
expect(response.ok()).toBeTruthy()
expect(response.headers()['content-type'])
.toContain('application/json')
await expect(response.json()).resolves.toMatchObject({ ok: true })
})| Goal | API test | Browser E2E |
|---|---|---|
| Status and body | Excellent | Indirect |
| Fast endpoint verification | Excellent | Slower |
| User experience | No | Excellent |
| Browser behavior | No | Yes |
| Auth cookie flow | Good | Excellent |
| Full UI integration | No | Yes |
API Security Testing

Defensive cases include malformed JSON, missing fields, invalid types, missing authentication, wrong ownership, oversized input where a limit exists, safe error bodies and disallowed methods. For cookie-authenticated mutations, apply the architecture-specific CSRF guidance from Blog #27. If an endpoint accepts external URLs, verify the allowlist and outbound service boundary described in Blog #28. Dependency hygiene connects to Blog #30.
Rate Limits, CORS, Caching and Revalidation
Test CORS and OPTIONS only when the API intentionally supports cross-origin clients. Avoid a permissive wildcard on private credentialed endpoints. If rate limiting exists, use a deterministic test adapter to cover an allowed request, the actual threshold, 429 and reset behavior without flooding shared infrastructure.
For caching and revalidation, verify behavior against the installed Next.js version. A mutation that calls revalidatePath or revalidateTag should ultimately make fresh data observable. A mocked function call alone proves only that a collaborator was invoked.
Test Data, Isolation and Time
Never copy customer data into fixtures. Use generated emails, fake UUIDs, factories and seeded records. Each test should own its records instead of relying on a previous case. Generate unique values for parallel workers and clean only scoped data. For timestamp or expiry rules, control time in focused tests rather than adding fixed sleeps.
Project-Specific Test Matrix
Because this publishing repository contains no Next.js Route Handlers, the truthful matrix is empty. In the real application, inventory three to six safe targets: a public GET, validated mutation, protected resource, database-backed path, expected error and one real HTTP check. Never replace missing architecture with fictional routes.
| Detected endpoint | Classification | Test status |
|---|---|---|
No app/**/route.ts or route.js found | Not applicable in this PHP publishing repository | No runtime API tests created or claimed |
A Practical GET Route Handler Test Plan
Begin by writing down the GET contract before choosing mocks. Identify whether the route returns a collection, a single resource, a computed view or a proxy response. Record the successful shape, the meaning of an empty result, accepted search parameters, visibility rules and any deliberate cache policy. This prevents a test from quietly deciding behavior that the API never promised.
For a public collection, a compact suite often covers a normal result, an empty array, supported filtering, malformed pagination and a controlled repository failure. If the endpoint returns pagination metadata, assert the fields that clients depend on rather than snapshotting the entire payload. If sort order is contractual, seed records whose order is unambiguous and assert the identifiers in order. If order is not contractual, avoid making it one accidentally.
A single-resource GET usually needs valid-found, valid-missing and malformed-identifier cases. An authenticated route adds missing identity and disallowed ownership. Keep these cases independent: a 404 assertion should not rely on another test deleting the record first. Whether a protected API returns 403 or intentionally hides existence with 404 is an application security decision; document it and test it consistently.
Service failure cases should begin at a boundary the test controls. Configure the repository or service substitute to throw a recognized error and confirm the route maps it to a safe response. Do not simulate a production outage by changing a real connection string or directing the route toward production infrastructure. The goal is to prove error translation, not to create collateral failure.
A Practical POST Route Handler Test Plan
For a mutation, separate transport parsing from domain validation. An empty body, malformed JSON and an unsupported content type are transport problems. A parsed object with a missing name or invalid email is a schema problem. A duplicate unique field, stale version or invalid state transition is a domain conflict. Keeping those categories visible makes status choices, logs and client messages easier to reason about.
A successful creation test should construct the same kind of Request a real client sends, call the route or send HTTP, assert the documented success status and inspect the returned representation. Then query the isolated database or repository to prove that normalized values were persisted. A response containing the submitted input is not proof that a write succeeded.
Malformed JSON deserves its own test when the handler promises a clean response. Wrap parsing at the appropriate boundary, return the project's chosen safe status and avoid echoing the raw parser exception. Validation failures should return stable field or issue information that clients can handle, but not internal schema objects whose shape may change across a library upgrade.
If the operation can conflict, create the prerequisite state inside the same case. For example, insert the first unique record and submit the second through the endpoint. Assert the documented conflict response and prove that only the intended record remains. Do not invent a 409 merely because it appears in a status table; use it only when the production route implements a real conflict.
Authentication Fixtures Without Secret Leakage
Authentication setup should be reusable without becoming invisible magic. A fixture can create a test account, obtain test-only state and expose either an isolated APIRequestContext or a browser context. Keep role and ownership explicit in the case so a reviewer can tell why access should be allowed. Store generated state outside version control and ensure traces, screenshots and CI artifacts have an appropriate retention policy.
Playwright distinguishes API contexts that share a BrowserContext cookie jar from standalone isolated contexts. Sharing state is useful when a browser journey and API assertion belong to one user flow. An isolated context is better when a test must prove that no browser cookies are present. Choose intentionally; otherwise an API test may pass because an unrelated browser login silently supplied the session.
Never place a bearer token, session value or webhook secret directly in a test title, assertion message, URL or snapshot. Load test-only credentials from the supported environment mechanism and redact request logging. A failed assertion that prints every response header can expose Set-Cookie values even when the production response is correct.
Uploads, Streaming and Health Routes
Only test these categories when they exist. For an upload route, use small harmless fixtures and cover the allowed type, rejected type, missing file, enforced size boundary, authentication and a controlled storage failure. Client-provided filename and content type are untrusted metadata; production validation determines the contract. Do not add malware samples or huge payloads to an ordinary application suite.
A streaming Route Handler needs Web Streams-aware assertions. Verify meaningful chunks, completion and safe cancellation behavior instead of converting a potentially unbounded stream into one giant buffer. Streaming behavior can depend on the runtime and deployment adapter, so retain at least one real HTTP check when it is a critical product feature.
A health endpoint should return minimal operational information, an intentional status and no credentials, query text, dependency addresses or exact private infrastructure versions. If readiness depends on services, keep checks bounded so the health request cannot amplify an outage. Test both the healthy contract and a controlled dependency failure without disconnecting production systems.
Review Tests as Security and Product Contracts
When reviewing an API test, ask what a pass actually proves. A pure schema test proves accepted input rules, not HTTP parsing. A direct handler test proves handler and Web API behavior, not filesystem routing or Proxy behavior. A mocked repository proves collaboration with that interface, not SQL constraints. A Playwright request proves the running route, but may still use a substitute service. Accurate labels prevent false confidence.
Review negative cases beside production code. Confirm authentication occurs before sensitive work, authorization checks the specific resource, validation precedes expensive operations, and failure mapping is neutral. For a denial test, inspect protected state afterward. For an external URL feature, verify rejection occurs before the outbound service connects. For a webhook, verify parsing and mutation happen only after signature validation.
Also inspect observability. Logs should help operators distinguish malformed input, expected denial, dependency failure and unexpected exceptions without storing credentials or sensitive request bodies. Tests can capture a controlled logger and assert an event category or correlation identifier, but should avoid freezing complete log strings. The public response and private diagnostic event have different audiences.
Finally, review parallel behavior. Unique email addresses, slugs, IDs, ports and database namespaces prevent worker collisions. Cleanup belongs in resilient hooks or disposable infrastructure and must remain scoped even when an assertion fails. If a group must run serially, document the shared resource; serial ordering should not hide accidental dependence between ordinary cases.
From Route Inventory to Running Suite
Start with a route inventory generated from the real repository. For each handler, record methods, input sources, authentication, authorization, persistence, outbound services, cookies, response headers, cache behavior and destructive side effects. Classify the endpoint as public read, public mutation, authenticated, authorization-sensitive, webhook, internal, database-backed, external proxy, upload or health. One endpoint may have several classifications.
Rank the inventory by consequence and change frequency. A payment webhook, ownership-sensitive mutation and externally consumed contract normally deserve more layers than an internal static lookup. Select three to six initial targets and create a matrix linking each risk to a test type. This is more useful than chasing a coverage percentage that treats trivial and critical lines equally.
Reuse the installed runner, aliases, setup files, fixtures and folder conventions. Do not create a second Vitest or Playwright configuration because a tutorial uses a different directory. Confirm the package manager from its lockfile, the Node version from the project's declared tooling, and the exact Next.js and React versions from the manifest and resolved lock data. Read current migration notes before changing async context or cache assumptions.
Run focused Vitest cases locally, then the complete configured suite. Build the application before Playwright, start it with test-only environment variables and wait for an observable readiness endpoint or log signal instead of a fixed sleep. Run the selected API and browser projects that CI will execute. Report passed, failed and skipped counts exactly as produced; if a tool is absent, say that it was not run.
Common Next.js API Testing Mistakes
- Testing only the happy path.
- Unit-testing helpers but never the HTTP contract.
- Using E2E for every small rule.
- Using a production database or production API keys.
- Calling uncontrolled third parties in every run.
- Ignoring authorization and object ownership.
- Confusing 401 with 403.
- Returning 200 for every failure.
- Snapshotting huge JSON responses.
- Sharing mutable records or depending on test order.
- Hard-coding IDs that collide in parallel.
- Ignoring malformed JSON and safe error output.
- Mocking the entire Route Handler.
- Calling a handler directly and claiming full runtime coverage.
- Testing only the browser UI.
- Assuming old caching behavior.
- Skipping webhook signature checks where webhooks exist.
Next.js 16 API Testing Best Practices
- Unit-test pure business rules.
- Reuse real validation schemas.
- Test Request and Response behavior.
- Add real HTTP tests for critical endpoints.
- Assert status, body and contract headers.
- Separate authentication from authorization cases.
- Use isolated databases and unique records.
- Mock narrow service boundaries selectively.
- Assert persisted state for important mutations.
- Protect secrets, webhook signatures and traces.
- Verify visible cache and revalidation behavior.
- Keep tests independent and concurrency-safe.
- Follow current framework and tool documentation.
Production API Testing Strategy
A practical CI pipeline runs fast checks first and widens confidence gradually. Build before Playwright so the suite tests the production-shaped application. Provide test-only environment values through CI secrets, start isolated dependencies, retain redacted artifacts for failures, and never run destructive requests against production.
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 TestingPublished
#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 Route Handlers in Next.js 16?
Use Vitest for pure rules and focused request/response tests, then Playwright API requests for critical endpoints over a running Next.js server.
Can I unit test a Route Handler?
Yes, but keep pure validation and business rules separate where practical. A direct handler test is closer to integration testing than a pure unit test.
Should I call the Route Handler directly in tests?
Direct calls are useful for handler and Web Request/Response behavior, but they bypass the Next.js router and do not replace real HTTP coverage.
How do I test a Next.js API over real HTTP?
Start the application in a safe test environment and use Playwright APIRequestContext through its request fixture and configured baseURL.
Can Playwright test APIs?
Yes. Playwright APIRequestContext supports GET, POST, PUT, PATCH, DELETE, headers, cookies, JSON data and response assertions.
How do I test GET Route Handlers?
Cover successful data, empty results, valid and invalid query values, authorization where required, missing resources and controlled service failures.
How do I test POST Route Handlers?
Cover valid JSON, malformed JSON, missing fields, validation failures, authorization, persistence, conflict behavior and safe failures that apply to the route.
How do I test request validation?
Test the same production schema with valid, invalid and boundary inputs, then verify the Route Handler maps failures to the documented response contract.
How do I test query parameters?
Construct URLs with expected, missing, malformed and out-of-range values. Use NextRequest only when the handler needs Next.js-specific request helpers.
How do I test dynamic route parameters?
Use the async context parameter shape required by the installed Next.js version and verify valid, malformed, found and missing identifiers.
How do I test authenticated APIs?
Use test-only identities and cover missing, valid and safely reproducible invalid or expired credentials without logging their values.
What is the difference between 401 and 403?
401 means valid authentication is missing or unacceptable. 403 means the caller is identified but is not permitted to perform the operation.
How do I test authorization and resource ownership?
Create a resource for test user A, authenticate as test user B, request the protected operation and verify denial plus unchanged state.
How do I test database writes safely?
Use a disposable test database or isolated schema, unique records and deterministic cleanup or transaction rollback. Assert both response and persisted state.
Should API tests use the production database?
No. Mutation suites must refuse production configuration and use isolated test data and credentials.
How do I test external API calls?
Mock a narrow service wrapper in focused tests and reserve a provider sandbox or controlled substitute for selected integration or E2E checks.
How do I test webhook Route Handlers?
Use provider-approved test signing, then cover a valid event, missing or invalid signature, malformed payload and duplicate delivery only when idempotency exists.
How do I test Route Handler errors?
Trigger known failure boundaries and assert the status, safe public body and unchanged state. Never expose stacks, SQL details, tokens or internal hosts.
How do I test rate limiting?
Against a local deterministic limiter, verify allowed requests, the configured threshold, 429 behavior and reset logic without bombarding production.
Should API tests run in CI?
Yes. Run focused tests before build, then start a test server and run selected Playwright API and browser journeys with isolated services.
Current Official References
- Next.js Route Handlers guide
- Next.js testing guides
- NextRequest reference
- NextResponse reference
- Vitest guide
- Vitest global mocking
- Playwright API testing
- Web Request API
- Web Response API
Next Steps
Audit the actual Next.js application before creating its route matrix. Start with one public read, one validated mutation and one protected contract, then add focused Vitest coverage and a real HTTP Playwright check. Continue with Blog #35 for sessions, protected routes, roles and authenticated browser flows.
