How to Implement Next.js API Routes in the App Directory
Next.js API routes in the App Router are implemented by creating route.ts (or .js) files inside app/api/ directories that export uppercase HTTP method handlers like GET, POST, or PUT, which receive a NextRequest object and must return a standard Response or NextResponse.
Next.js 13 introduced the App Router, fundamentally changing how developers implement nextjs api routes within the app/ folder structure. Unlike the legacy pages/api pattern, the App Router uses a file-system based approach with specific naming conventions and handler signatures. This guide examines the implementation details from the vercel/next.js repository to show you exactly how App Router API routes work under the hood.
File Structure and Naming Conventions
Unlike the pages/api directory where any file became an endpoint, nextjs api routes in the App Router require a specific convention. You create a directory structure inside app/api/ and place a file named route.ts, route.js, or route.tsx at the leaf level.
For example:
app/api/hello/route.ts→/api/helloapp/api/users/[id]/route.ts→/api/users/123
Exporting HTTP Method Handlers
Each route file exports functions named after HTTP methods in uppercase: GET, POST, PUT, DELETE, PATCH, OPTIONS, and HEAD. These functions receive a NextRequest object and should return a Response or NextResponse.
According to the source code in packages/next/src/server/route-modules/app-route/helpers/auto-implement-methods.ts, the framework validates that only uppercase method names are exported and auto-implements HEAD by delegating to GET if not explicitly defined.
Simple GET Route Example
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({ message: 'Hello from the App Router!' })
}
Handling Multiple Methods
import { NextResponse } from 'next/server'
// GET – list users
export async function GET() {
const users = await fetch('https://api.example.com/users').then(r => r.json())
return NextResponse.json(users)
}
// POST – create a new user
export async function POST(req: Request) {
const data = await req.json()
const created = await fetch('https://api.example.com/users', {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
}).then(r => r.json())
return NextResponse.json(created, { status: 201 })
}
Core Architecture of Next.js API Routes
The implementation of nextjs api routes in the App Router relies on several sophisticated systems working together to provide static generation, caching, and edge runtime support.
Route Module Initialization
When the server receives a request, the AppRouteRouteModule class (defined in packages/next/src/server/route-modules/app-route/module.ts) initializes by reading the dynamic export from your route file. The constructor (lines 40-78) processes these flags:
export const dynamic = 'force-dynamic'setsforceDynamic: true, ensuring the route runs on every requestexport const dynamic = 'force-static'setsforceStatic: true, allowing static generation at build timeexport const dynamic = 'error'setsdynamicShouldError: true, throwing an error if dynamic APIs are accessed during static generation
Request Store and Async Storage
The module creates a request store via createRequestStoreForAPI that wraps the standard Web API Request into a NextRequest object. This process, handled within the route module's execution logic, adds Next.js-specific helpers like nextUrl, cookies, and search parameters.
Your handler executes inside several async storage contexts (workUnitAsyncStorage, workAsyncStorage, actionAsyncStorage) that provide static-generation detection, caching support, and server-action capabilities. This architecture allows nextjs api routes to participate in the same rendering pipeline as React Server Components.
Response Normalization
After your handler finishes executing, the AppRouteRouteModule normalizes the response (lines 82-89 of module.ts) and merges any mutable cookies using appendMutableCookies. If you return a standard Web API Response or use NextResponse.json(), the framework handles header normalization and status code validation.
If you attempt to use forbidden APIs like NextResponse.rewrite() inside an API route handler, the module throws a clear error (lines 35-41 of module.ts) with documentation links, as rewrite operations are reserved for middleware and page routes.
Dynamic Rendering and Caching Controls
Control whether your nextjs api routes run at build time or request time using the dynamic export. Combine this with revalidate for fine-grained cache control.
import { NextResponse } from 'next/server'
export const dynamic = 'force-dynamic' // Always render on each request
export const revalidate = 0 // Disable ISR cache
export async function GET() {
// This runs on every request because of `dynamic = 'force-dynamic'`
const now = new Date().toISOString()
return NextResponse.json({ now })
}
The dynamic export is processed by the AppRouteRouteModule constructor to set flags in the WorkStore that determine static generation behavior.
Accessing Request Context and Helpers
Handlers receive a NextRequest object (defined in packages/next/src/server/web/spec-extension/request.ts) that extends the standard Web API Request with Next.js-specific properties.
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
// `request` is a `NextRequest` with `nextUrl`, `cookies`, etc.
const locale = request.nextUrl.searchParams.get('lang') ?? 'en'
const session = request.cookies.get('session')?.value ?? null
return NextResponse.json({ locale, authenticated: !!session })
}
You can also return standard Web API Response objects for raw text or custom headers:
export async function GET() {
const body = 'Plain text response'
return new Response(body, {
status: 200,
headers: { 'Content-Type': 'text/plain' },
})
}
Summary
- Next.js API routes in the App Router use
route.tsfiles insideapp/api/directories with uppercase HTTP method exports (GET,POST, etc.) - The
AppRouteRouteModuleclass inpackages/next/src/server/route-modules/app-route/module.tsorchestrates request handling, validation, and response normalization - Use
export const dynamicto control rendering behavior:'force-dynamic'for SSR,'force-static'for build-time generation, or'error'to fail on dynamic APIs - Handlers receive a
NextRequestobject with helpers likenextUrland cookies, and should returnResponseorNextResponse - The framework auto-implements
HEADfromGETand manages async storage contexts (workUnitAsyncStorage,workAsyncStorage,actionAsyncStorage) for caching and server actions
Frequently Asked Questions
Can I use the pages/api directory alongside app/api routes?
Yes, Next.js supports incremental adoption. You can have both pages/api and app/api routes in the same project, though routes in app/api follow the new Route Handler convention with uppercase method exports while pages/api uses default exports. Note that app/api routes take precedence over pages/api routes when URLs collide.
Why do I get an error when using NextResponse.rewrite() in an API route?
The NextResponse.rewrite() API is designed for middleware and page routes, not API Route Handlers. According to the source code in packages/next/src/server/route-modules/app-route/module.ts (lines 35-41), using rewrite inside an app router API handler throws a clear error because the route module expects a direct response, not a rewrite instruction.
How do I handle dynamic route parameters in app/api routes?
Dynamic segments work identically to page routes. Create a directory with brackets like app/api/users/[id]/route.ts. The handler receives a params object as the second argument: export async function GET(request: Request, { params }: { params: { id: string } }) { ... }. The AppRouteRouteModule processes these parameters through parsed-url-query-to-params.ts helpers.
What is the difference between export const dynamic and export const revalidate?
The dynamic export controls whether the route is rendered statically at build time or dynamically at request time ('force-static', 'force-dynamic', or 'error'). The revalidate export (or revalidate option in fetch) controls how long statically generated responses remain valid in the cache when using Incremental Static Regeneration (ISR). Use dynamic for rendering behavior, revalidate for cache duration on static responses.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →