Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
menu
  1. Home
  2. /Web Development
  3. /Nextjs
  4. /Route Handlers Intro
Your Progress0%
0 of 70 completed

NextJS Topics

Getting Started

  • What is Next.js?
  • Next.js vs React - Key Differences
  • Creating Your First Next.js 15 Project
  • Next.js Project Structure Explained
  • App Router vs Pages Router

Routing Fundamentals

  • File-Based Routing Basics
  • Creating Pages with page.tsx
  • Dynamic Routes and Route Parameters
  • Catch-All and Optional Catch-All Routes
  • Route Groups for Organization
  • Parallel Routes
  • Intercepting Routes

Layouts and Pages

  • Understanding Layouts
  • Root Layout and Global Configuration
  • Nested Layouts
  • Templates vs Layouts
  • Loading States with loading.tsx
  • Error Handling with error.tsx

Server and Client Components

  • Understanding Server Components
  • Client Components with 'use client'
  • When to Use Server vs Client Components
  • Component Composition Patterns
  • Passing Props Between Server and Client
  • Server Component Patterns and Best Practices

Data Fetching

  • Fetching Data in Server Components
  • Parallel and Sequential Data Fetching
  • Data Fetching Patterns and Strategies
  • Caching and Revalidation
  • Revalidate and Cache Tags
  • Handling Loading and Error States in Data Fetching

Navigation and Links

  • Link Component Basics
  • useRouter Hook for Programmatic Navigation
  • usePathname and useSearchParams Hooks
  • Active Links and Navigation States
  • Redirects and Navigation Guards

Styling in Next.js

  • CSS Modules in Next.js
  • Tailwind CSS Setup and Configuration
  • Global Styles and CSS Variables
  • Next.js Font Optimization

Images and Media

  • Next.js Image Component Basics
  • Image Optimization and Best Practices
  • Working with Static Assets

Forms and Data Mutations

  • Understanding Server Actions
  • Form Handling with Server Actions
  • Form Validation and Error Handling
  • useFormStatus and useFormState Hooks
  • Revalidating Data After Mutations
  • Optimistic Updates

Metadata and SEO

  • Static Metadata Configuration
  • Dynamic Metadata Generation
  • Open Graph and Social Media Cards
  • Sitemap and Robots.txt

API Routes and Route Handlers

  • Route Handlers Introduction
  • GET and POST Request Handlers
  • Dynamic API Routes
  • Request and Response Objects
  • API Error Handling and Status Codes

Middleware and Advanced Features

  • Introduction to Middleware
  • Authentication with Middleware
  • Environment Variables and Configuration
  • Streaming and Suspense
  • Not Found and Global Error Pages

Deployment and Optimization

  • Understanding Static and Dynamic Rendering
  • generateStaticParams for Static Generation
  • Build and Production Optimization
  • Deploying to Vercel
  • Alternative Deployment Options
  • Performance Monitoring and Analytics

Real-World Projects

  • Project 1: E-commerce Product Catalog
  • Project 2: Dashboard with Authentication

Route Handlers Introduction

Creating API endpoints in the App Router

Route Handlers let you create custom API endpoints using Web Request and Response APIs. They replace the Pages Router's API Routes with a more powerful, standards-based approach. Create route.ts files to handle HTTP methods like GET, POST, PUT, DELETE—building RESTful APIs directly in your Next.js app. Let's master API development in the App Router!

What Are Route Handlers?

❌ Pages Router API Routes

TYPESCRIPT
// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  res.status(200).json({ message: 'Hello' });
}

// Issues:
// ❌ Node.js-specific APIs
// ❌ Custom NextApiRequest/Response
// ❌ Less flexible
// ❌ Separate from App Router

✅ App Router Route Handlers

TYPESCRIPT
// app/api/hello/route.ts
export async function GET(request: Request) {
  return Response.json({ message: 'Hello' });
}

// Benefits:
// ✅ Web standard Request/Response
// ✅ Same APIs as fetch()
// ✅ More portable code
// ✅ Integrated with App Router
// ✅ Streaming support
// ✅ Edge runtime compatible

Key Differences from Pages Router

  • Standard APIs: Web Request/Response instead of Node.js-specific
  • HTTP method exports: Export GET, POST, etc. functions
  • File name: route.ts instead of any name in pages/api/
  • Location: Can be anywhere in app/, not just app/api/
  • Async by default: All handlers are async

Creating Your First Route Handler

Simple GET Handler

app/api/hello/route.ts
export async function GET(request: Request) {
  return new Response('Hello, Next.js!');
}

// Access at: http://localhost:3000/api/hello
// Returns: "Hello, Next.js!" (plain text)

// ✅ Export GET function
// ✅ Receives Request object
// ✅ Returns Response object
// ✅ Async by default

Returning JSON

app/api/data/route.ts
export async function GET(request: Request) {
  const data = {
    message: 'Hello from API',
    timestamp: new Date().toISOString(),
  };

  return Response.json(data);
}

// Access at: /api/data
// Returns:
// {
//   "message": "Hello from API",
//   "timestamp": "2024-01-15T10:30:00.000Z"
// }

// ✅ Response.json() sets Content-Type: application/json
// ✅ Automatic JSON serialization
// ✅ No manual JSON.stringify needed

Using NextResponse

app/api/users/route.ts
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
  ];

  return NextResponse.json(users);
}

// NextResponse extends Response with:
// ✅ Cookies helper methods
// ✅ Redirect helpers
// ✅ Rewrite support
// ✅ Next.js-specific features

// Use Response.json() for simple cases
// Use NextResponse.json() for advanced features

Supported HTTP Methods

All HTTP Methods

app/api/posts/route.ts
// GET - Retrieve data
export async function GET(request: Request) {
  const posts = await db.posts.findMany();
  return Response.json(posts);
}

// POST - Create new resource
export async function POST(request: Request) {
  const body = await request.json();
  const post = await db.posts.create({ data: body });
  return Response.json(post, { status: 201 });
}

// PUT - Update entire resource
export async function PUT(request: Request) {
  const body = await request.json();
  const post = await db.posts.update({
    where: { id: body.id },
    data: body,
  });
  return Response.json(post);
}

// PATCH - Partial update
export async function PATCH(request: Request) {
  const body = await request.json();
  const post = await db.posts.update({
    where: { id: body.id },
    data: body,
  });
  return Response.json(post);
}

// DELETE - Remove resource
export async function DELETE(request: Request) {
  const { searchParams } = new URL(request.url);
  const id = searchParams.get('id');
  
  await db.posts.delete({ where: { id } });
  return Response.json({ success: true });
}

// HEAD - Same as GET but no body
export async function HEAD(request: Request) {
  return new Response(null, {
    status: 200,
    headers: {
      'Content-Type': 'application/json',
    },
  });
}

// OPTIONS - Describe communication options
export async function OPTIONS(request: Request) {
  return new Response(null, {
    status: 200,
    headers: {
      'Allow': 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS',
    },
  });
}

// ✅ Each method is a separate function
// ✅ Export functions you need
// ✅ Omit unsupported methods (returns 405)

Route Handler Rules

Rule 1: Cannot Coexist with page.tsx

TYPESCRIPT
// ❌ BAD: Both in same directory
app/
  blog/
    page.tsx    // Page component
    route.ts    // API handler
    // ERROR! Conflict!

// ✅ GOOD: Separate routes
app/
  blog/
    page.tsx    // Page at /blog
  api/
    blog/
      route.ts  // API at /api/blog

// A route segment can be EITHER a page OR an API endpoint

Rule 2: Route Handlers Can Be Anywhere

TYPESCRIPT
// Route handlers don't have to be in /api
app/
  rss.xml/
    route.ts    // RSS feed at /rss.xml
  
  sitemap.xml/
    route.ts    // Sitemap at /sitemap.xml
  
  blog/
    feed/
      route.ts  // Blog feed at /blog/feed
  
  api/
    posts/
      route.ts  // Traditional API at /api/posts

// ✅ Place route handlers where they make sense
// ✅ /api is convention, not requirement

Rule 3: Layout and Template Apply

TYPESCRIPT
// Layouts and middleware apply to route handlers
app/
  layout.tsx        // Root layout
  api/
    layout.tsx      // API layout (optional)
    posts/
      route.ts      // Inherits layouts
  
middleware.ts       // Runs for route handlers too

// ✅ Middleware runs before route handlers
// ✅ Layouts wrap route handlers (if returning HTML)
// ✅ Most APIs return JSON, so layouts don't apply

Reading Request Data

Request Body (JSON)

app/api/posts/route.ts
export async function POST(request: Request) {
  // Parse JSON body
  const body = await request.json();
  
  console.log(body);
  // { title: 'My Post', content: 'Post content' }
  
  // Validate and use
  if (!body.title) {
    return Response.json(
      { error: 'Title is required' },
      { status: 400 }
    );
  }
  
  const post = await db.posts.create({
    data: {
      title: body.title,
      content: body.content,
    },
  });
  
  return Response.json(post, { status: 201 });
}

// ✅ request.json() parses JSON body
// ✅ Async method
// ✅ Validates and processes

Request Body (FormData)

app/api/upload/route.ts
export async function POST(request: Request) {
  // Parse form data
  const formData = await request.formData();
  
  const title = formData.get('title') as string;
  const file = formData.get('file') as File;
  
  console.log('Title:', title);
  console.log('File:', file.name, file.size);
  
  // Process file...
  
  return Response.json({ 
    success: true,
    filename: file.name,
  });
}

// ✅ request.formData() for multipart/form-data
// ✅ Access fields with .get()
// ✅ Files are File objects

URL Search Params

app/api/search/route.ts
export async function GET(request: Request) {
  // Parse URL
  const { searchParams } = new URL(request.url);
  
  // Get individual params
  const query = searchParams.get('q');
  const page = searchParams.get('page') || '1';
  const limit = searchParams.get('limit') || '10';
  
  console.log('Query:', query);
  console.log('Page:', page);
  
  // Use params
  const results = await db.posts.findMany({
    where: {
      title: {
        contains: query || '',
      },
    },
    skip: (parseInt(page) - 1) * parseInt(limit),
    take: parseInt(limit),
  });
  
  return Response.json(results);
}

// GET /api/search?q=nextjs&page=2&limit=20
// ✅ Parse URL with new URL()
// ✅ Access searchParams
// ✅ Default values with ||

Request Headers

app/api/auth/route.ts
export async function GET(request: Request) {
  // Read headers
  const authorization = request.headers.get('authorization');
  const userAgent = request.headers.get('user-agent');
  const contentType = request.headers.get('content-type');
  
  console.log('Auth:', authorization);
  console.log('User-Agent:', userAgent);
  
  // Verify auth
  if (!authorization) {
    return Response.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }
  
  // Process...
  return Response.json({ success: true });
}

// ✅ request.headers.get(name)
// ✅ Case-insensitive
// ✅ Returns null if not found

Setting Response Properties

Response Headers

app/api/data/route.ts
export async function GET(request: Request) {
  const data = { message: 'Hello' };
  
  return Response.json(data, {
    status: 200,
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=30',
      'X-Custom-Header': 'My Value',
    },
  });
}

// ✅ Set headers in second argument
// ✅ Cache-Control for caching
// ✅ Custom headers with X- prefix

Status Codes

app/api/posts/route.ts
export async function POST(request: Request) {
  const body = await request.json();
  
  // 201 Created
  const post = await db.posts.create({ data: body });
  return Response.json(post, { status: 201 });
}

export async function GET(request: Request) {
  const posts = await db.posts.findMany();
  
  if (posts.length === 0) {
    // 404 Not Found
    return Response.json(
      { error: 'No posts found' },
      { status: 404 }
    );
  }
  
  // 200 OK (default)
  return Response.json(posts);
}

// Common status codes:
// 200 - OK
// 201 - Created
// 204 - No Content
// 400 - Bad Request
// 401 - Unauthorized
// 403 - Forbidden
// 404 - Not Found
// 500 - Internal Server Error

Cookies

app/api/session/route.ts
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const body = await request.json();
  
  // Create response
  const response = NextResponse.json({
    success: true,
    user: body.username,
  });
  
  // Set cookie
  response.cookies.set('session', 'session-token-123', {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7, // 1 week
  });
  
  return response;
}

export async function GET(request: Request) {
  // Read cookie from request
  const session = request.headers.get('cookie')
    ?.split('; ')
    .find(c => c.startsWith('session='))
    ?.split('=')[1];
  
  if (!session) {
    return Response.json(
      { error: 'No session' },
      { status: 401 }
    );
  }
  
  return Response.json({ session });
}

// ✅ NextResponse for cookie helpers
// ✅ httpOnly for security
// ✅ secure in production

Practical Route Handler Examples

Example 1: Simple REST API

app/api/todos/route.ts
import { NextResponse } from 'next/server';

// In-memory storage (use database in production)
let todos = [
  { id: '1', text: 'Learn Next.js', completed: false },
  { id: '2', text: 'Build an app', completed: false },
];

// GET - List all todos
export async function GET(request: Request) {
  return NextResponse.json(todos);
}

// POST - Create new todo
export async function POST(request: Request) {
  const body = await request.json();
  
  const newTodo = {
    id: Date.now().toString(),
    text: body.text,
    completed: false,
  };
  
  todos.push(newTodo);
  
  return NextResponse.json(newTodo, { status: 201 });
}

// DELETE - Delete all todos
export async function DELETE(request: Request) {
  todos = [];
  return NextResponse.json({ success: true });
}

// ✅ Complete CRUD API
// ✅ GET, POST, DELETE methods
// ✅ RESTful patterns

Example 2: External API Proxy

app/api/weather/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const city = searchParams.get('city');
  
  if (!city) {
    return Response.json(
      { error: 'City parameter required' },
      { status: 400 }
    );
  }
  
  try {
    // Fetch from external API
    const apiKey = process.env.WEATHER_API_KEY;
    const response = await fetch(
      `https://api.weather.com/v1/current?city=${city}&key=${apiKey}`
    );
    
    if (!response.ok) {
      throw new Error('Weather API failed');
    }
    
    const data = await response.json();
    
    // Return proxied data
    return Response.json({
      city,
      temperature: data.temp,
      conditions: data.conditions,
    });
  } catch (error) {
    return Response.json(
      { error: 'Failed to fetch weather' },
      { status: 500 }
    );
  }
}

// GET /api/weather?city=London
// ✅ Proxy external API
// ✅ Hide API key from client
// ✅ Transform response
// ✅ Error handling

Example 3: Webhook Receiver

app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';

export async function POST(request: Request) {
  const body = await request.text();
  const signature = headers().get('stripe-signature');
  
  if (!signature) {
    return Response.json(
      { error: 'No signature' },
      { status: 400 }
    );
  }
  
  try {
    // Verify webhook signature
    const event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
    
    // Handle event
    switch (event.type) {
      case 'payment_intent.succeeded':
        const paymentIntent = event.data.object;
        console.log('Payment succeeded:', paymentIntent.id);
        // Update database...
        break;
      
      case 'payment_intent.failed':
        console.log('Payment failed');
        // Handle failure...
        break;
    }
    
    return Response.json({ received: true });
  } catch (error) {
    console.error('Webhook error:', error);
    return Response.json(
      { error: 'Webhook verification failed' },
      { status: 400 }
    );
  }
}

// ✅ Webhook endpoint
// ✅ Signature verification
// ✅ Event processing
// ✅ Secure

Route Handlers Structure

Organization of API routes in the app directory

appImportant

Select a file or folder to see details

Route Handler Best Practices

1. Use Proper HTTP Methods

TYPESCRIPT
// ✅ GOOD: RESTful methods
export async function GET() { /* Get resources */ }
export async function POST() { /* Create resource */ }
export async function PUT() { /* Full update */ }
export async function PATCH() { /* Partial update */ }
export async function DELETE() { /* Delete resource */ }

// ❌ BAD: POST for everything
export async function POST(request: Request) {
  const { action } = await request.json();
  if (action === 'get') { /* ... */ }
  if (action === 'delete') { /* ... */ }
}

// Use proper HTTP semantics

2. Return Appropriate Status Codes

TYPESCRIPT
// ✅ GOOD: Meaningful status codes
// Created
return Response.json(resource, { status: 201 });

// Not found
return Response.json({ error: 'Not found' }, { status: 404 });

// Bad request
return Response.json({ error: 'Invalid data' }, { status: 400 });

// ❌ BAD: Always 200
return Response.json({ error: 'Not found' }); // Still 200!

// Status codes communicate meaning

3. Validate Input

TYPESCRIPT
// ✅ GOOD: Validate before processing
export async function POST(request: Request) {
  const body = await request.json();
  
  if (!body.email || !body.password) {
    return Response.json(
      { error: 'Email and password required' },
      { status: 400 }
    );
  }
  
  // Process...
}

// Even better: Use Zod
import { z } from 'zod';

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

export async function POST(request: Request) {
  const body = await request.json();
  const result = schema.safeParse(body);
  
  if (!result.success) {
    return Response.json(
      { errors: result.error.flatten() },
      { status: 400 }
    );
  }
  
  // Use validated result.data
}

4. Handle Errors Gracefully

TYPESCRIPT
// ✅ GOOD: Try-catch with error handling
export async function GET(request: Request) {
  try {
    const data = await db.posts.findMany();
    return Response.json(data);
  } catch (error) {
    console.error('Database error:', error);
    return Response.json(
      { error: 'Failed to fetch posts' },
      { status: 500 }
    );
  }
}

// Don't expose internal errors to clients

5. Use Environment Variables for Secrets

TYPESCRIPT
// ✅ GOOD: Environment variables
const apiKey = process.env.API_KEY;
const dbUrl = process.env.DATABASE_URL;

// ❌ BAD: Hard-coded secrets
const apiKey = 'sk_live_abc123';  // NEVER!

// Never commit secrets to Git

Key Takeaways

  • route.ts file - creates Route Handlers
  • Export HTTP methods - GET, POST, PUT, PATCH, DELETE
  • Web Request/Response - standard APIs
  • Response.json() - return JSON responses
  • Cannot coexist with page.tsx - either page or API
  • Read with request.json() - parse request body
  • Status codes matter - use appropriate codes
  • NextResponse - for cookies and advanced features

What's Next?

You've mastered Route Handler basics! Next, we'll dive deep into GET and POST Request Handlers—building complete CRUD APIs, handling different request types, working with query parameters, and implementing RESTful patterns. You'll build production-ready API endpoints!

We'll cover GET request patterns, POST request handling, request body parsing, query parameter processing, and complete API examples.

🔒 Security First

Always validate input, use appropriate authentication, never expose secrets, handle errors gracefully, and use HTTPS in production. Route Handlers are publicly accessible by default—protect sensitive endpoints!

Test Your Understanding

Question 1 of 4

What file creates a Route Handler in the App Router?

Master Route Handlers in Next.js! Learn to create API endpoints with route.ts and build RESTful APIs.

Previous
Sitemap and Robots.txt
Next
GET and POST Request Handlers

Master Next.js APIs

Join 2,000+ developers building powerful APIs with Next.js. Get the next lesson on GET and POST handlers - 100% FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

NextJS Tutorials

0 of 70 completed

Your Progress0%

Topics

Getting Started

  • What is Next.js?
  • Next.js vs React - Key Differences
  • Creating Your First Next.js 15 Project
  • Next.js Project Structure Explained
  • App Router vs Pages Router

Routing Fundamentals

  • File-Based Routing Basics
  • Creating Pages with page.tsx
  • Dynamic Routes and Route Parameters
  • Catch-All and Optional Catch-All Routes
  • Route Groups for Organization
  • Parallel Routes
  • Intercepting Routes

Layouts and Pages

  • Understanding Layouts
  • Root Layout and Global Configuration
  • Nested Layouts
  • Templates vs Layouts
  • Loading States with loading.tsx
  • Error Handling with error.tsx

Server and Client Components

  • Understanding Server Components
  • Client Components with 'use client'
  • When to Use Server vs Client Components
  • Component Composition Patterns
  • Passing Props Between Server and Client
  • Server Component Patterns and Best Practices

Data Fetching

  • Fetching Data in Server Components
  • Parallel and Sequential Data Fetching
  • Data Fetching Patterns and Strategies
  • Caching and Revalidation
  • Revalidate and Cache Tags
  • Handling Loading and Error States in Data Fetching

Navigation and Links

  • Link Component Basics
  • useRouter Hook for Programmatic Navigation
  • usePathname and useSearchParams Hooks
  • Active Links and Navigation States
  • Redirects and Navigation Guards

Styling in Next.js

  • CSS Modules in Next.js
  • Tailwind CSS Setup and Configuration
  • Global Styles and CSS Variables
  • Next.js Font Optimization

Images and Media

  • Next.js Image Component Basics
  • Image Optimization and Best Practices
  • Working with Static Assets

Forms and Data Mutations

  • Understanding Server Actions
  • Form Handling with Server Actions
  • Form Validation and Error Handling
  • useFormStatus and useFormState Hooks
  • Revalidating Data After Mutations
  • Optimistic Updates

Metadata and SEO

  • Static Metadata Configuration
  • Dynamic Metadata Generation
  • Open Graph and Social Media Cards
  • Sitemap and Robots.txt

API Routes and Route Handlers

  • Route Handlers Introduction
  • GET and POST Request Handlers
  • Dynamic API Routes
  • Request and Response Objects
  • API Error Handling and Status Codes

Middleware and Advanced Features

  • Introduction to Middleware
  • Authentication with Middleware
  • Environment Variables and Configuration
  • Streaming and Suspense
  • Not Found and Global Error Pages

Deployment and Optimization

  • Understanding Static and Dynamic Rendering
  • generateStaticParams for Static Generation
  • Build and Production Optimization
  • Deploying to Vercel
  • Alternative Deployment Options
  • Performance Monitoring and Analytics

Real-World Projects

  • Project 1: E-commerce Product Catalog
  • Project 2: Dashboard with Authentication
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo