Route Handlers in Next.js 14: Tutorial & Examples

Updated: December 14, 2023 By: Wolf Post a comment

Next.js 13 and 14 have brought with them a host of new features and enhancements (along with the /app directory) that have propelled them further as a top choice among developers. The star of the show in this release is the introduction of Route Handlers, a powerful feature that provides an intuitive way to manage and control backend API endpoints in your applications. This article will delve deep into the world of Route Handlers, their features, and how they can be effectively used in modern Next.js applications.

Understanding Route Handlers

Route Handlers usher in a revolutionary approach to managing custom request handlers for specific routes by leveraging the web Request and Response APIs. A remarkable attribute of Route Handlers is that they can only be accessed within the /app directory, a stark contrast with API routes that reside within the /pages directory.

export const dynamic = 'force-dynamic' // defaults to auto
export async function GET(request: Request) {}

These handlers can be defined within a route.js or route.ts file located in the /app directory. They can be nested within the /app directory, much like page.js and layout.js, but it’s important to note that a route.js file cannot coexist at the same route segment level as a page.js.

Supported HTTP Methods in Route Handlers

Route Handlers offer support for various HTTP methods, including GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS. If an unsupported method is invoked, Next.js will return a 405 error (Method Not Allowed) response.

Enhancing Request and Response with NextRequest and NextResponse APIs

Apart from supporting native Request and Response, Next.js extends them with NextRequest and NextResponse. These extensions equip developers with additional helpers for advanced use cases.

Caching in Route Handlers

Next.js enables caching by default when using the GET method with the Response object.

export async function GET() {
  const res = await fetch('https://api.slingacademy.com', {
    headers: {
      'Content-Type': 'application/json',
    },
  })
  const data = await res.json()
  return Response.json({ data })
}

Here, it’s important to note that Response.json() is valid only from TypeScript 5.2 onwards. For applications using lower versions of TypeScript, NextResponse.json() can be used for typed responses.

Opting out of Caching

Next.js allows developers to opt out of caching by using the Request object with the GET method, using other HTTP methods, using Dynamic Functions like cookies() and headers(), or manually specifying dynamic mode in Segment Config Options.

An example of opting out of caching using the GET method:

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const id = searchParams.get('id')
  const res = await fetch(`https://data.mongodb-api.com/product/${id}`, {
    headers: {
      'Content-Type': 'application/json',
      'API-Key': process.env.DATA_API_KEY!,
    },
  })
  const product = await res.json()
  return Response.json({ product })
}

POST method can also be used to evaluate Route Handler dynamically:

export async function POST() {
  const res = await fetch('https://data.mongodb-api.com/...', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'API-Key': process.env.DATA_API_KEY!,
    },
    body: JSON.stringify({ time: new Date().toISOString() }),
  })
  const data = await res.json()
  return Response.json(data)
}

Just like API Routes, Route Handlers can be used for a variety of cases, including managing form submissions.

Route Resolution in Next.js 14

A route can be considered as a fundamental routing primitive. They do not participate in layouts or client-side navigations like a page. It’s important that a route.js file cannot exist at the same route as a page.js.

Examples of Using Route Handlers

The following examples illustrate how to combine Route Handlers with other Next.js APIs and features.

Revalidating Cached Data

You can revalidate cached data using the next.revalidate option:

export async function GET() {
  const res = await fetch('https://data.mongodb-api.com/...', {
    next: {
      revalidate: 60, // Revalidate every 60 seconds
    },
  })
  const data = await res.json()
  return Response.json(data)
}

Alternatively, you can use the revalidate segment config option:

export const revalidate = 60

Dynamic Functions

As said earlier, Route Handlers can be used with dynamic functions from Next.js, like cookies() and headers().

Cookies

Cookies can be read with cookies from next/headers. This server function can be called directly in a Route Handler, or nested within another function.

import { cookies } from 'next/headers'
export async function GET(request: Request) {
  const cookieStore = cookies()
  const token = cookieStore.get('token')
  return new Response('Hello, Next.js!', {
    status: 200,
    headers: {
      'Set-Cookie': `token=${token.value}`,
    },
  })
}

Headers

Headers can be read with headers from next/headers. This server function can be called directly in a Route Handler, or nested within another function.

import { headers } from 'next/headers'
export async function GET(request: Request) {
  const headersList = headers()
  const referer = headersList.get('referer')
  return new Response('Hello, Next.js!', {
    status: 200,
    headers: {
      referer: referer,
    },
  })
}

Redirects

Redirects can be achieved using redirect from next/navigation.

import { redirect } from 'next/navigation'
export async function GET(request: Request) {
  redirect('https://www.slingacademy.com')
}

Dynamic Route Segments

Route Handlers can use Dynamic Segments to create request handlers from dynamic data.

export async function GET( request: Request, { params }: { params: { slug: string } }) {
  const slug = params.slug // 'a', 'b', or 'c'
}

URL Query Parameters

The request object passed to the Route Handler is a NextRequest instance, which has additional convenience methods, including handling query parameters more easily.

import { type NextRequest } from 'next/server'
export function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const query = searchParams.get('query') // query is "hello" for /api/search?query=hello
}

Streaming

Streaming is commonly used in combination with Large Language Models (LLMs), such as OpenAI.

import OpenAI from 'openai'
import { OpenAIStream, StreamingTextResponse } from 'ai'
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
})
export const runtime = 'edge'
export async function POST(req: Request) {
  const { messages } = await req.json()
  const response = await openai.chat.completions.create({
    model: 'gpt-3.5-turbo',
    stream: true,
    messages,
  })
  const stream = OpenAIStream(response)
  return new StreamingTextResponse(stream)
}

Request Body

You can read the Request body using standard Web API methods:

export async function POST(request: Request) {
  const res = await request.json()
  return Response.json({ res })
}

Request Body FormData

FormData can be read using the request.formData() function:

export async function POST(request: Request) {
  const formData = await request.formData()
  const name = formData.get('name')
  const email = formData.get('email')
  return Response.json({ name, email })
}

CORS

CORS headers can be set on a Response using standard Web API methods:

export const dynamic = 'force-dynamic' // defaults to auto
export async function GET(request: Request) {
  return new Response('Hello, Next.js!', {
    status: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  })
}

Edge and Node.js Runtimes

Route Handlers have an isomorphic Web API to support both Edge and Node.js runtimes seamlessly, including support for streaming.

Non-UI Responses

Route Handlers can be used to return non-UI content. Note that sitemap.xml, robots.txt, app icons, and open graph images all have built-in support.

export const dynamic = 'force-dynamic' // defaults to auto
export async function GET() {
  return new Response(`<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
  <channel>
    <title>Next.js Tutorials</title>
    <link>https://www.slingacademy.com/cat/next-js/</link>
    <description>The React Framework for the Web</description>
  </channel>
</rss>`)
}

Segment Config Options

Route Handlers use the same route segment configuration as pages and layouts.

export const dynamic = 'auto'
export const dynamicParams = true
export const revalidate = false
export const fetchCache = 'auto'
export const runtime = 'nodejs'
export const preferredRegion = 'auto'

Final Thoughts

By understanding the capabilities of Route Handlers in Next.js 14, developers can leverage this powerful feature to create more efficient and dynamic web applications. With numerous options for customization and control, Route Handlers are a significant step forward in the evolution of Next.js.