How Middleware Works in Next.js 14 App Router?

Middleware in Next.js is a powerful feature that allows you to run code before a request is completed. This can be particularly useful for tasks such as authentication, rate limiting, and injecting headers. As of Next.js 14, middleware operates in the Edge runtime, offering performance benefits due to its proximity to the user. This tutorial will guide you through how middleware works, with a focus on using TypeScript.

Pasted%20image%2020240925173203

What is Middleware?

In Next.js, middleware are functions that run before the request is completed. They allow you to manipulate requests and responses, perform checks, or mutate state before continuing with a standard request-response cycle. Then, based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly. Middleware runs before cached content and routes are matched.

Middleware Naming and Placement

Middleware files should be named middleware.ts or middleware.js and be placed besides the app folder or in any specific directory under the /app directory. This convention allows Next.js to recognize and execute these files at the appropriate stages of the request lifecycle.

- app/
  - about/
    - page.tsx
    - middleware.ts
  - contact/
    - page.tsx
  - middleware.ts
- middleware.ts

With src folder

-src
	- app/
	  - about/
	    - page.tsx
	    - middleware.ts
	  - contact/
	    - page.tsx
	  - middleware.ts
	- middleware.ts

Use Case

Checking User Session

Let's create a middleware to check if the user is authenticated by examining their session token.

Declare a Middleware in TypeScript

Create a middleware.ts file in your app directory or project root:

import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
import cookieparser from 'cookieparser';

// Secret key for JWT. Normally you should store this in environment variables.
const SECRET_KEY = process.env.SECRET_KEY || 'your-secret-key';

export async function middleware(req: NextRequest) {
  const { cookies } = req;
  const token = cookies.token;

  if (!token) {
    return NextResponse.redirect('/login');
  }

  try {
    // Verify the JWT token
    const decoded = jwt.verify(token, SECRET_KEY);
    req.user = decoded as any; // Add user info to request
    return NextResponse.next();
  } else {
    return NextResponse.redirect('/login');
  }
}

Other Possible Use Cases

Authentication and Authorization:

  • Checking user roles
  • Ensuring users are logged in before accessing restricted routes

Rate Limiting:

  • Throttling requests to prevent abuse

A/B Testing:

  • Serving different versions of the page based on cookies or headers

Localization:

  • Redirecting users to region-specific pages based on their IP

Analytics:

  • Collecting data about user requests

Skelton Middleware functions are exported from a file named middleware.ts or middleware.js. Here is a skeleton in TypeScript:

import { NextRequest, NextResponse } from 'next/server';

export async function middleware(req: NextRequest) {
  // Logic goes here
  return NextResponse.next();
}

Matchers in Middleware

Matchers restrict middleware execution to specified paths. Use the matcher configuration in middleware.ts:

Match only these paths

// by adding this export in the middleware, it configures middleware to execute only for these paths
export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*'],
};

Match All except these

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico, sitemap.xml, robots.txt (metadata files)
     */
    '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
  ],
}

Bypass middleware for certain API's

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico, sitemap.xml, robots.txt (metadata files)
     */
    {
      source:
        '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
 
    {
      source:
        '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
      has: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },
 
    {
      source:
        '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
      has: [{ type: 'header', key: 'x-present' }],
      missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],
    },
  ],
}

Rules for Matchers

Configured matchers:

  1. MUST start with /
  2. Can include named parameters: /about/:path matches /about/a and /about/b but not /about/a/c
  3. Can have modifiers on named parameters (starting with :): /about/:path* matches /about/a/b/c because * is zero or more? is zero or one and + one or more
  4. Can use regular expression enclosed in parenthesis: /about/(.*) is the same as /about/:path*

Path Checking

How to check path in the middleware of next.js? You can use conditional logic to execute middleware based on specific conditions. Here's an example of checking the request path:

export async function middleware(req: NextRequest) {
  if (req.nextUrl.pathname.startsWith('/admin')) {
    // Logic for admin routes
  } else {
    // Logic for other routes
  }
  return NextResponse.next();
}

Rewrite and Redirect

Next.js provides the NextResponse object to handle responses within middleware. You can use it to rewrite URLs, redirect requests, or send custom responses.

Rewrite Example:

You can use the NextResponse.rewrite method to manipulate the request path without changing the URL in the browser:

import { NextRequest, NextResponse } from 'next/server';

export async function middleware(req: NextRequest) {
  if (req.nextUrl.pathname.startsWith('/old-path')) {
    return NextResponse.rewrite(new URL('/new-path', req.url));
  }
  return NextResponse.next();
}

Redirect Example:

You can use the NextResponse.redirect method to send users to a different URL completely:

import { NextRequest, NextResponse } from 'next/server';

export async function middleware(req: NextRequest) {
  if (!req.cookies.token) {
    return NextResponse.redirect(new URL('/login', req.url));
  }
  return NextResponse.next();
}

Cookies in Middleware

Cookies are a common way to handle session data and user authentication. You can read cookies directly from the NextRequest object and manipulate them accordingly.

Example: Reading and Setting Cookies

import { NextRequest, NextResponse } from 'next/server';

export async function middleware(req: NextRequest) {
  const token = req.cookies.get('token');

  if (!token) {
    const response = NextResponse.redirect(new URL('/login', req.url));
    response.cookies.set('lastVisited', req.url);
    return response;
  }

  // To set a cookie
  const response = NextResponse.next();
  response.cookies.set('visitedAt', new Date().toISOString());
  return response;
}

Headers and CORS

How to set header and cors in the middleware.ts? You might need to set custom headers or manage Cross-Origin Resource Sharing (CORS) in your middleware.

Example: Setting Headers and Managing CORS

import { NextRequest, NextResponse } from 'next/server';

export async function middleware(req: NextRequest) {
  const response = NextResponse.next();

  // Set custom headers
  response.headers.set('X-Custom-Header', 'MyCustomValue');

  // Handling CORS
  response.headers.set('Access-Control-Allow-Origin', '*');
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');

  return response;
}

Edge Runtime

One key aspect of Next.js middleware as of version 14 is that it runs in the Edge runtime instead of Node.js. This brings significant performance improvements because the Edge runtime is designed for low latency, distributed systems, and it runs closer to the user.

Key Differences

  • Global Scope: Unlike Node.js, the Edge runtime doesn't have access to Node.js' global scope. For example, common modules like fs are not available.
  • Performance: The Edge runtime is optimized for speed and distributed execution, making it excellent for running middleware that handles routing, redirections, and simple logic fast.

Example:

// middleware.ts

import { NextRequest, NextResponse } from 'next/server';

export async function middleware(req: NextRequest) {
  // Logic goes here
  return NextResponse.next();
}

// Edge runtime: Fast and distributed

Conclusion

Middleware in Next.js 14 offers powerful, flexible tools for handling requests. Whether youโ€™re validating sessions, setting headers, managing CORS, or redirecting based on conditions, middleware gives you the control you need to build robust web applications.

Summary

  • Middleware Naming and Placement: Place middleware.ts or middleware.js in the root or under specific directories in the /app folder.
  • Use Cases: Authentication, rate limiting, A/B testing, localization, and analytics.
  • Matchers: Utilize matchers to specify paths where middleware should be executed.
  • Conditional Statements: Use conditional logic to handle different scenarios within the middleware.
  • NextResponse: Use methods like rewrite and redirect to handle request manipulations.
  • Cookies: Read, set, and manipulate cookies easily.
  • Headers and CORS: Customize headers and manage CORS policies.
  • Edge Runtime: Benefit from the performance improvements of running middleware on the Edge runtime.
Clap here if you liked the blog