Bird
Raised Fist0
NextJSframework~15 mins

Route handlers (route.ts) in NextJS - Deep Dive

Choose your learning style10 modes available

Start learning this pattern below

Jump into concepts and practice - no test required

or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
Overview - Route handlers (route.ts)
What is it?
Route handlers in Next.js are special files named route.ts that define how your app responds to HTTP requests like GET or POST. They let you write server-side code directly inside your app folder to handle data fetching, form submissions, or API calls. Instead of separate API folders, route handlers live alongside your pages and components, making your app structure simpler and clearer. They use modern TypeScript syntax to keep your code safe and easy to understand.
Why it matters
Without route handlers, managing server logic in Next.js apps can get messy and disconnected from the UI, making it harder to maintain and slower to develop. Route handlers solve this by placing server code right next to the UI code it supports, speeding up development and reducing bugs. This means faster, more reliable apps that are easier to update and scale. For beginners, it makes learning full-stack development smoother because everything is in one place.
Where it fits
Before learning route handlers, you should understand basic Next.js pages and React components. Knowing HTTP methods like GET and POST helps too. After mastering route handlers, you can explore advanced API routes, middleware, and server actions in Next.js, which build on these concepts to create powerful, dynamic web apps.
Mental Model
Core Idea
Route handlers are like the app’s front desk, deciding how to respond to each visitor’s request based on the type of request they make.
Think of it like...
Imagine a restaurant where the route handler is the host who listens to what each guest wants—whether a drink, a meal, or a reservation—and directs the kitchen or staff to prepare exactly that. Each HTTP method (GET, POST) is like a different type of request from the guest.
┌───────────────┐
│ HTTP Request  │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ Route Handler │
│ (route.ts)    │
│  GET → fetch  │
│  POST → save  │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ Server Logic  │
│ (DB, APIs)    │
└───────────────┘
Build-Up - 7 Steps
1
FoundationWhat is a Route Handler File
🤔
Concept: Route handlers are special files named route.ts that define server-side responses for HTTP requests.
In Next.js, you create a file called route.ts inside the app folder or its subfolders. This file exports functions named after HTTP methods like GET or POST. Each function runs when the app receives a request of that type at that route. For example, a GET function returns data, and a POST function processes data sent by the client.
Result
You have a file that listens for HTTP requests and sends back responses based on the request type.
Understanding that route.ts files are the entry points for server-side logic in Next.js apps is key to organizing backend code alongside frontend UI.
2
FoundationBasic GET Route Handler Example
🤔
Concept: A GET function in route.ts handles requests to fetch data and returns a response.
Example route.ts: export async function GET(request: Request) { return new Response('Hello from GET!'); } This code listens for GET requests and replies with a simple text message.
Result
When you visit the route in a browser or fetch it, you see 'Hello from GET!' as the response.
Seeing how a simple GET function returns a response helps you grasp how route handlers connect HTTP requests to server responses.
3
IntermediateHandling POST Requests with JSON
🤔Before reading on: do you think POST handlers automatically parse JSON body, or do you need to do it manually? Commit to your answer.
Concept: POST functions receive data from the client and often need to parse JSON from the request body.
Example POST handler: export async function POST(request: Request) { const data = await request.json(); // process data return new Response('Data received'); } Here, request.json() reads the JSON sent by the client.
Result
The server reads the sent data and responds confirming receipt.
Knowing that you must manually parse JSON in POST handlers prevents bugs and clarifies how data flows from client to server.
4
IntermediateUsing TypeScript Types in Route Handlers
🤔Before reading on: do you think TypeScript types are enforced at runtime in route handlers? Commit to your answer.
Concept: TypeScript helps catch errors during development but does not enforce types at runtime in route handlers.
You can define interfaces for expected data: interface UserData { name: string; age: number; } export async function POST(request: Request) { const data: UserData = await request.json(); // TypeScript checks data shape here return new Response('User saved'); } But at runtime, you should still validate data manually.
Result
Your code is safer during development, but you must handle unexpected data at runtime.
Understanding the limits of TypeScript in route handlers helps you write safer, more robust server code.
5
IntermediateOrganizing Multiple HTTP Methods
🤔
Concept: You can export multiple functions in route.ts to handle different HTTP methods for the same route.
Example: export async function GET() { return new Response('GET response'); } export async function POST(request: Request) { const data = await request.json(); return new Response('POST response'); } This lets one file handle all main request types for a route.
Result
Your route responds correctly depending on whether the client sends GET or POST.
Knowing how to handle multiple methods in one file keeps your API organized and easy to maintain.
6
AdvancedError Handling and Response Status Codes
🤔Before reading on: do you think throwing an error inside a route handler automatically sends a 500 response? Commit to your answer.
Concept: You control error responses by returning Response objects with appropriate status codes; unhandled errors cause generic 500 responses.
Example: export async function GET() { try { // some logic return new Response('Success', { status: 200 }); } catch (error) { return new Response('Error occurred', { status: 500 }); } } This way, you send clear success or error messages with correct HTTP codes.
Result
Clients receive meaningful status codes and messages, improving debugging and UX.
Understanding explicit error handling in route handlers prevents silent failures and improves API reliability.
7
ExpertRoute Handlers and Edge Runtime
🤔Before reading on: do you think route handlers always run on the Node.js server, or can they run closer to users? Commit to your answer.
Concept: Next.js route handlers can run in the Edge Runtime, which runs server code closer to users for faster responses but with some API limitations.
By adding 'export const runtime = "edge";' in route.ts, your handler runs on the Edge Runtime. This means faster cold starts and lower latency but no access to Node.js APIs like file system or certain modules. Example: export const runtime = 'edge'; export async function GET() { return new Response('Edge response'); } This is great for lightweight APIs and global apps.
Result
Your route handler runs on a global network of servers, speeding up responses worldwide.
Knowing about the Edge Runtime lets you optimize route handlers for performance and scalability in production.
Under the Hood
Route handlers are special exports in route.ts files that Next.js detects during build. When a request matches the route, Next.js calls the matching HTTP method function (GET, POST, etc.) with a Request object. This function runs server-side, either in a Node.js environment or the Edge Runtime, and returns a Response object. Next.js then sends this response back to the client. The Request and Response follow the Web Fetch API standard, making server code similar to client-side fetch usage.
Why designed this way?
Next.js designed route handlers to unify frontend and backend code in the app directory, simplifying full-stack development. Using standard Web Fetch API objects makes the server code familiar to frontend developers. Supporting the Edge Runtime allows apps to run server code closer to users for better performance. This design replaces older API routes folders with a more integrated, modern approach.
┌───────────────┐
│ Client sends  │
│ HTTP Request  │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ Next.js       │
│ Route Matcher │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ route.ts file │
│ exports GET() │
│ exports POST()│
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ Server Runtime│
│ (Node.js or   │
│ Edge Runtime) │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ Returns       │
│ Response      │
└──────┬────────┘
       │
       ▼
┌───────────────┐
│ Client gets   │
│ HTTP Response │
└───────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Do route handlers automatically parse JSON bodies for POST requests? Commit to yes or no.
Common Belief:Route handlers automatically parse JSON bodies, so you can use the data directly.
Tap to reveal reality
Reality:You must manually call request.json() to parse the JSON body inside POST handlers.
Why it matters:Assuming automatic parsing leads to runtime errors and crashes when trying to access undefined data.
Quick: Do you think TypeScript types in route handlers enforce data shape at runtime? Commit to yes or no.
Common Belief:TypeScript types guarantee the data shape at runtime in route handlers.
Tap to reveal reality
Reality:TypeScript only checks types during development; at runtime, you must validate data yourself.
Why it matters:Relying on TypeScript alone can cause unexpected bugs if invalid data reaches your server logic.
Quick: Do route handlers always run on the Node.js server? Commit to yes or no.
Common Belief:Route handlers always run on the Node.js server environment.
Tap to reveal reality
Reality:Route handlers can run in the Edge Runtime, which has different capabilities and restrictions.
Why it matters:Not knowing this can cause bugs when using Node.js-only APIs in edge-enabled handlers.
Quick: Does exporting multiple HTTP methods in one route.ts file cause conflicts? Commit to yes or no.
Common Belief:You can only export one HTTP method function per route.ts file.
Tap to reveal reality
Reality:You can export multiple HTTP method functions (GET, POST, etc.) in the same file without conflict.
Why it matters:Believing otherwise leads to fragmented code and unnecessary file splitting.
Expert Zone
1
Route handlers use the Web Fetch API’s Request and Response objects, which differ from Node.js’s native HTTP objects, requiring adaptation when using third-party libraries.
2
Edge Runtime route handlers have strict limits on CPU time and memory, so heavy computations or long-running tasks should be offloaded or avoided.
3
Middleware and route handlers can interact, but middleware runs before route handlers and can modify requests or responses, enabling powerful request control.
When NOT to use
Route handlers are not ideal for very complex backend logic requiring persistent connections, heavy computation, or advanced database transactions. In such cases, use dedicated backend services or serverless functions outside Next.js. Also, avoid edge runtime for handlers needing Node.js APIs or long execution times.
Production Patterns
In production, route handlers are used to build lightweight APIs, handle form submissions, and serve dynamic data close to users. Developers often combine route handlers with middleware for authentication and caching. Edge runtime is leveraged for global apps needing low latency, while Node.js runtime is chosen for handlers requiring full Node.js features.
Connections
Serverless Functions
Route handlers are a built-in, file-based way to write serverless functions inside Next.js apps.
Understanding route handlers helps grasp serverless concepts where backend code runs on demand without managing servers.
HTTP Protocol
Route handlers directly implement HTTP methods and status codes to communicate with clients.
Knowing HTTP basics clarifies why route handlers use GET, POST, and status codes to control web interactions.
Edge Computing
Route handlers can run on the Edge Runtime, which is a form of edge computing placing code near users.
Learning about route handlers introduces edge computing concepts that improve app speed and scalability globally.
Common Pitfalls
#1Forgetting to parse JSON body in POST handlers.
Wrong approach:export async function POST(request: Request) { const data = request.body; return new Response('Received'); }
Correct approach:export async function POST(request: Request) { const data = await request.json(); return new Response('Received'); }
Root cause:Misunderstanding that request.body is a stream and requires async parsing with request.json().
#2Using Node.js-only APIs in Edge Runtime route handlers.
Wrong approach:export const runtime = 'edge'; import fs from 'fs'; export async function GET() { const data = fs.readFileSync('file.txt', 'utf-8'); return new Response(data); }
Correct approach:export async function GET() { // Use fetch or other edge-compatible APIs instead return new Response('Edge compatible response'); }
Root cause:Not realizing Edge Runtime restricts Node.js modules like 'fs' for security and performance.
#3Assuming TypeScript types prevent runtime errors in route handlers.
Wrong approach:interface Data { name: string; } export async function POST(request: Request) { const data: Data = await request.json(); // no validation return new Response('OK'); }
Correct approach:export async function POST(request: Request) { const data = await request.json(); if (typeof data.name !== 'string') { return new Response('Invalid data', { status: 400 }); } return new Response('OK'); }
Root cause:Confusing compile-time type checking with runtime data validation.
Key Takeaways
Route handlers in Next.js let you write server-side code directly in route.ts files to handle HTTP requests like GET and POST.
They use the Web Fetch API’s Request and Response objects, making server code familiar and consistent with client-side fetch usage.
You must manually parse JSON bodies and handle errors explicitly to build reliable APIs.
Route handlers can run in Node.js or the Edge Runtime, each with different capabilities and use cases.
Understanding route handlers bridges frontend and backend development, enabling faster, cleaner full-stack apps.

Practice

(1/5)
1. What is the main purpose of a route.ts file in Next.js?
easy
A. To configure database connections
B. To style components using CSS modules
C. To create client-side React components
D. To define server-side code that handles HTTP requests for a specific URL

Solution

  1. Step 1: Understand the role of route.ts

    The route.ts file is used in Next.js to write server-side code that responds to HTTP requests for a specific route.
  2. Step 2: Compare with other options

    Styling, client components, and database configs are handled elsewhere, not in route.ts.
  3. Final Answer:

    To define server-side code that handles HTTP requests for a specific URL -> Option D
  4. Quick Check:

    Route handlers = server code for URLs [OK]
Hint: Route handlers handle server requests per URL [OK]
Common Mistakes:
  • Confusing route.ts with client component files
  • Thinking route.ts is for styling
  • Assuming route.ts manages database directly
2. Which of the following is the correct way to export a GET handler in route.ts?
easy
A. export async function GET(request: NextRequest) { return NextResponse.json({ message: 'Hi' }) }
B. export function Get() { return 'Hello' }
C. export async function get() { return new Response('Hello') }
D. export async function fetch() { return 'Hello' }

Solution

  1. Step 1: Recall Next.js route handler syntax

    Next.js expects exported async functions named exactly by HTTP method in uppercase, e.g., GET, with NextRequest parameter and returning NextResponse.
  2. Step 2: Check each option

    export async function GET(request: NextRequest) { return NextResponse.json({ message: 'Hi' }) } matches correct syntax: async, uppercase GET, parameter, and returns NextResponse. Others have wrong function names, casing, or return types.
  3. Final Answer:

    export async function GET(request: NextRequest) { return NextResponse.json({ message: 'Hi' }) } -> Option A
  4. Quick Check:

    Uppercase method + NextRequest + NextResponse = correct [OK]
Hint: Use uppercase HTTP method and NextRequest param [OK]
Common Mistakes:
  • Using lowercase method names like get instead of GET
  • Missing NextRequest parameter
  • Returning plain string instead of NextResponse
3. Given this route.ts code, what will be the JSON response body when a GET request is made?
import { NextResponse } from 'next/server';

export async function GET() {
  return NextResponse.json({ success: true, data: [1, 2, 3] });
}
medium
A. undefined
B. {"success":true,"data":[1,2,3]}
C. {"error":"Method not allowed"}
D. [1, 2, 3]

Solution

  1. Step 1: Analyze the GET handler return

    The GET function returns NextResponse.json with an object containing success: true and data: [1, 2, 3].
  2. Step 2: Understand JSON response format

    This creates a JSON response with the exact object serialized as a JSON string.
  3. Final Answer:

    {"success":true,"data":[1,2,3]} -> Option B
  4. Quick Check:

    NextResponse.json outputs JSON string [OK]
Hint: NextResponse.json sends exact JSON object [OK]
Common Mistakes:
  • Expecting just the array without wrapping object
  • Confusing error responses with success
  • Thinking response is undefined
4. Identify the error in this route.ts code snippet:
export async function POST(request: NextRequest) {
  const data = await request.json();
  return new Response(JSON.stringify(data));
}

export async function GET() {
  return new Response('Hello');
}
medium
A. Missing import of NextRequest
B. GET function must accept a request parameter
C. POST handler should return NextResponse, not Response
D. No error, code is correct

Solution

  1. Step 1: Check imports

    The code uses NextRequest but does not import it from 'next/server'. This causes a runtime error.
  2. Step 2: Validate other parts

    GET handler can omit request parameter if unused. Returning Response is allowed but NextResponse is preferred; not an error. So main error is missing import.
  3. Final Answer:

    Missing import of NextRequest -> Option A
  4. Quick Check:

    Using NextRequest requires import [OK]
Hint: Always import NextRequest when used [OK]
Common Mistakes:
  • Forgetting to import NextRequest
  • Thinking GET must have request param
  • Confusing Response and NextResponse as errors
5. You want to create a route handler in route.ts that responds to both GET and POST requests. The GET returns a JSON list of users, and the POST adds a user from the request body and returns the updated list. Which code snippet correctly implements this behavior?
hard
A. export async function GET(request) { return new Response(JSON.stringify(['Alice', 'Bob'])); } export async function POST(request) { const data = await request.json(); return new Response(JSON.stringify(['Alice', 'Bob'])); }
B. import { NextRequest } from 'next/server'; const users = []; export function get() { return users; } export function post(request) { users.push(request.body.name); return users; }
C. import { NextRequest, NextResponse } from 'next/server'; let users = ['Alice', 'Bob']; export async function GET() { return NextResponse.json(users); } export async function POST(request: NextRequest) { const newUser = await request.json(); users.push(newUser.name); return NextResponse.json(users); }
D. import { NextResponse } from 'next/server'; export async function GET() { return NextResponse.json(['Alice', 'Bob']); } export async function POST() { return NextResponse.json(['Alice', 'Bob', 'Charlie']); }

Solution

  1. Step 1: Check imports and state management

    import { NextRequest, NextResponse } from 'next/server'; let users = ['Alice', 'Bob']; export async function GET() { return NextResponse.json(users); } export async function POST(request: NextRequest) { const newUser = await request.json(); users.push(newUser.name); return NextResponse.json(users); } correctly imports NextRequest and NextResponse, and uses a mutable users array to store state between calls.
  2. Step 2: Verify GET and POST handlers

    GET returns current users as JSON. POST reads JSON body, adds new user, then returns updated list. This matches requirements.
  3. Step 3: Review other options

    import { NextRequest } from 'next/server'; const users = []; export function get() { return users; } export function post(request) { users.push(request.body.name); return users; } uses lowercase method names and no NextResponse, invalid in Next.js route handlers. export async function GET(request) { return new Response(JSON.stringify(['Alice', 'Bob'])); } export async function POST(request) { const data = await request.json(); return new Response(JSON.stringify(['Alice', 'Bob'])); } returns new Response but does not maintain state. import { NextResponse } from 'next/server'; export async function GET() { return NextResponse.json(['Alice', 'Bob']); } export async function POST() { return NextResponse.json(['Alice', 'Bob', 'Charlie']); } POST does not accept request or update users dynamically.
  4. Final Answer:

    import { NextRequest, NextResponse } from 'next/server'; let users = ['Alice', 'Bob']; export async function GET() { return NextResponse.json(users); } export async function POST(request: NextRequest) { const newUser = await request.json(); users.push(newUser.name); return NextResponse.json(users); } -> Option C
  5. Quick Check:

    Correct imports + state + async handlers = import { NextRequest, NextResponse } from 'next/server'; let users = ['Alice', 'Bob']; export async function GET() { return NextResponse.json(users); } export async function POST(request: NextRequest) { const newUser = await request.json(); users.push(newUser.name); return NextResponse.json(users); } [OK]
Hint: Use async GET/POST with NextRequest, NextResponse, and shared state [OK]
Common Mistakes:
  • Using lowercase method names instead of uppercase
  • Not importing NextRequest or NextResponse
  • Not maintaining state between requests
  • Ignoring async/await for request.json()