Skip to contentVisit my new website at chrisnicholas.dev
Article posted on()

How to Use Next.js Middleware
A quick explanation and some handy snippets of code

With the release of Next.js 12, Vercel Edge Functions have been announced, allowing for super speedy edge-optimised functions. They can also be used as helpful Next.js middleware functions. In this article I'll explain what they are & how to use them, before diving into a few examples.
Table of contents

With the release of Next.js 12, Vercel Edge Functions have been announced, allowing for super speedy edge-optimised functions. They can also be used as helpful Next.js middleware functions. In this article I'll explain what they are & how to use them, before diving into a few examples.

An interactive diagram of a web browser demonstrating a being blocked in Denmark, but not in Belgium and Czechia
I love Sweden!/pages/index.ts
DevTools

What are they?

Vercel edge functions are a kind of serverless function, similar to Vercel's API routes, except they're deployed in CDNs around the world, enabling much quicker connection times.

Within Next.js 12 they can be used as middleware—functions that run when users first connect to your website, and before the page loads. These functions can then be used to redirect, block, authenticate, filter, and so much more.

How to use them

In Next.js 12.2 place a single file named middleware.ts within the root directory of your project (next to package.json):

/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware (request: NextRequest) {
  return NextResponse.next()
}

This function will run before every page, API route, and file on your website starts to load. If NextResponse.next() is returned, or if there is no return value, pages will load as expected, as if there's no middleware:

An interactive diagram of a website displaying an index page
Welcome to the index!/pages/index.ts

Within this function we can run our tasks—but we'll get into that later.

Return a response

In previous versions of Next.js, middleware could return a Response body, but this is no longer allowed.

/middleware.ts
import type { NextRequest } from 'next/server'

export function middleware (request: NextRequest) {
  return new Response('Hello world!')
}
An diagram of a web browser displaying "hello world"
500 | Internal server error

Targeting certain pages

There are two different ways to target a particular page or file with middleware; using config, or by manually matching the URL. The following middleware function will only run on the /about page, or on any paths beginning with /articles:

/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// Runs only on matched pages, because of config
export function middleware (request: NextRequest) {
  // Runs for '/about' and pages starting with '/articles'
}

export const config = {
  matcher: ['/about', '/articles/:path*']
}

This can also be implemented without a matcher, though bear in mind that this will invoke middleware on every page load (because there are no matched pages):

/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// Runs on every page
export function middleware (request: NextRequest) {
  if (request.nextUrl.pathname === '/about') {
    // Runs for '/about'
  }

  if (request.nextUrl.pathname.startsWith('/articles')) {
    // Runs for '/articles'
  }
}

In this article we'll be combining both methods (where necessary), for the most performant and reusable middleware.

Before we start

We'll be using the URL interface in this article, so it's probably best you get acquainted with it (particularly hostname and pathname):

An interactive tool for showing you the output objects for URLs.
URL {
  hash: '#about',
  host: 'beta.example.com:8080',
  hostname: 'beta.example.com',
  href: 'https://beta.example.com:8080/people?name=chris#about',
  origin: 'https://beta.example.com:8080',
  password: '',
  pathname: '/people',
  port: '8080',
  protocol: 'https:',
  search: '?name=chris',
  searchParams: {
    name: 'chris'
  },
  username: ''
}
Note: searchParams is a JavaScript map, not an object.

Right, let's take a look at some examples!

Redirecting pages

Redirects (surprisingly!) allow you to redirect from one page to another. In this example we're redirecting from /2019 to /2022.

An interactive diagram of a website redirecting to another URL
Welcome to 2022!/pages/2022/index.ts
DevTools

Only absolute URLs work with redirect and rewrite, in the form of a string or a URL. In this article, we'll mostly be cloning request.nextUrl (the URL object for the current request) and modifying it, instead of creating a new URL:

/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// Beware loops when redirecting to the same directory
export function middleware (request: NextRequest) {
  if (request.nextUrl.pathname === '/2019')
    const url = request.nextUrl.clone()
    url.pathname = '/2022'
    return NextResponse.redirect(url)
  }
}

export const config = {
  matcher: ['/2019']
}

By cloning nextUrl in this way, we can preserve any parts of the original URL that aren't being changed, such as query strings, or subdomains, and only modify what we need.

Rewriting pages

Rewrites allow you serve a page at one location, whilst displaying the URL of another. They're handy for tidying up messy URLs, or for using subdomains to separate different sections within the same website. In this example, we're rewriting beta.example.com to example.com/beta:

An interactive diagram of a website rewriting its URL
Start with the beta./pages/beta/start.ts
DevTools

Here we're checking for visits on the hostname beta.example.com, and then serving example.com/beta instead. We're doing this by once again modifying a cloned nextUrl, changing the hostname, and adding /beta to the start of the current pathname.

/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware (request: NextRequest) {
  const hostname = request.headers.get('host')

  // If on beta.example.com, redirect to example.com/beta
  if (hostname === 'beta.example.com') {
    const url = request.nextUrl.clone()
    url.hostname = 'example.com'
    url.pathname = '/beta' + url.pathname
    return NextResponse.rewrite(url)
  }
}

In retaining the old url.pathname, we're making sure that all beta pages redirect, not just the index.

User agent checking

Next.js 12.2 provides a new userAgent feature that returns the connected client's user agent allowing us to, among other things, detect mobile devices:

An interactive diagram of a website displaying different domains loading depending on user device
Desktop home/pages/index.ts
DevTools

If we put our previous knowledge together we can now redirect mobile users from example.com to m.example.com, and then rewrite m.example.com to /pages/mobile/:

/middleware.ts
import { NextResponse, userAgent } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware (request: NextRequest) {
  const hostname = request.headers.get('host')
  const { device } = userAgent(request)

  // If example.com visited from mobile, redirect to m.example.com
  if (hostname === 'example.com' && device.type === 'mobile') {
    const url = request.nextUrl.clone()
    url.hostname = 'm.example.com'
    return NextResponse.redirect(url)
  }

  // If m.example.com visited, rewrite to /pages/mobile
  if (hostname === 'm.example.com') {
    const url = request.nextUrl.clone()
    url.pathname = '/mobile' +  url.pathname
    return NextResponse.rewrite(url)
  }

  return NextResponse.next()
}

Prevent access

Preventing access to files and directories is very simple with edge functions. In this example, all API routes are blocked unless a custom secret-key header is passed:

An interactive diagram of a website displaying an API being blocked & unblocked
{ success: true }/pages/api/query.ts
DevTools

All we're doing here is checking the secret-key header for the correct value, and redirecting to a custom error page if it isn't used:

/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const secretKey = 'artichoke'

export function middleware (request: NextRequest) {
  if (request.nextUrl.pathname === '/api/query') {
    const headerKey = request.headers.get('secret-key')

    // If secret keys match, allow access
    if (headerKey === secretKey) {
      return NextResponse.next()
    }

    // Otherwise, redirect to your custom error page
    const url = request.nextUrl.clone()
    url.pathname = '/unauthorised'
    return NextResponse.redirect(url)
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/api/query']
}

This API can then be accessed using fetch:

const result = await fetch('https://example.com/api/query', {
  headers: {
    'secret-key': 'artichoke'
  }
})

View counter

Edge functions are a reliable place to keep track of website views; no JavaScript has to be loaded by the page for them to run, unlike with client API calls.

An interactive diagram of a website displaying a view count
View count is 571/pages/counter.ts
DevTools

First we check to see whether a page is being accessed—we don't want this to run when APIs are called or files are loaded—and continue if so. We're making use of event.waitUntil() here, because it will allow us to run asynchronous code after the rest of the function has completed. Put simply, we won't add any additional delay by the database call because we'll process it after the user has begun loading the page.

/middleware.ts
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'

export function middleware (request: NextRequest, event: NextFetchEvent) {
  const { pathname } = request.nextUrl

  // Ignore files and API calls
  if (pathname.includes('.') || pathname.startsWith('/api')) {
    return NextResponse.next()
  }

  event.waitUntil(
    (async () => {
      // Add view to your database
      // ...
    })()
  )

  return NextResponse.next()
}

My website uses Upstash to keep track of views, a low-latency serverless database for Redis.

Advanced waitUntil() explanation

<OverlyComplexExplanation>

Cloudflare workers implement the service worker API. The second parameter for middleware() is an ExtendableEvent from this api, which provides the waitUntil() method. waitUntil() accepts a Promise parameter, and is essentially used to prevent computation from closing whilst the promise is unresolved. This is only necessary in service workers.

Promises are returned from async functions, which is why we're passing one as an argument for waitUntil(), and then immediately executing it. This means that any asynchronous code run within this function, will be run after middleware() has returned a value.

event.waitUntil(
  (async () => {
    // ...
  })()
)

However, bear in mind that any synchronous code will run before middleware() has completed, and only code after the await keyword will run after:

console.log(1)

event.waitUntil(
  (async () => {
    // ...

    console.log(2)
    await Promise.resolve()
    console.log(4)
  })()
)

console.log(3)
1 2 3 4

This probably won't be necessary, but if you need all your code to run after middleware() has returned a value, place it behind await Promise.resolve() like console.log(4) above. Alternatively, setTimeout within a new Promise will work too:

console.log(1)

event.waitUntil(new Promise(resolve => {
  setTimeout(async () => {
    // ...

    console.log(3)
    await Promise.resolve()
    console.log(4)

    resolve()
  })
}))

console.log(2)
1 2 3 4

</OverlyComplexExplanation>

If you'd be interested in an article fully explaining JavaScript timing (with interactive visualisations), let me know on Twitter.

Location filtering

Edge functions allow you to detect the location of the user's request, and make adjustments accordingly. In this example we're serving the regular website, unless a Danish user connects, where we'll serve a custom error page instead.

An interactive diagram of a web browser demonstrating a being blocked in Denmark, but not in Belgium and Czechia
I love Sweden!/pages/index.ts
DevTools

The request.geo object contains the two-letter country code we need. If the country code matches, we can then redirect to a custom error page:

/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware (request: NextRequest) {
  // request.geo.country is undefined in dev mode, 'US' as backup
  const country = request.geo.country || 'US'

  // If visited from Denmark
  if (country === 'DK') {
    const url = request.nextUrl.clone()
    url.pathname = '/forbidden'
    return NextResponse.redirect(url)
  }
}

Set theme by sunlight

In another post I wrote about detecting the user's sunlight levels, and setting the theme to dark or light mode accordingly. Vercel Edge Functions give us access to the approximate location of our users, so we can use this to set a theme before the page has even loaded.

An interactive diagram of a website displaying dark and light mode at different times
View my articles./pages/articles.ts
DevTools

Here we're setting the current theme as a cookie, which we'll then retrieve client-side. We're also checking to see if the theme is already set, and skipping the function if it is.

/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import SunCalc from 'suncalc'

export function middleware (request: NextRequest) {
  // Skip if theme already set
  if (request.cookies.get('theme')) {
    return NextResponse.next()
  }

  // Get location
  const { longitude = 0, latitude = 0 } = request.geo

  // Get theme (explanation in related article below)
  const now = new Date()
  const { sunrise, sunset } = SunCalc.getTimes(now, longitude, latitude)
  let mode = ''
  if (now < sunrise || now > sunset) {
    mode = 'dark'
  } else {
    mode = 'light'
  }

  // Set cookie and continue
  const response = new NextResponse()
  response.cookies.set('theme', mode)
  return response
}

Using the new URL import feature in Next.js 12 we can use js-cookie directly from Unpkg to get the cookie on the front end, (not necessary, just a new feature!), and then enable the correct theme:

import Cookies from 'https://unpkg.com/js-cookie@3.0.1/dist/js.cookie.min.js'

// 'dark' or 'light'
const theme = Cookies.get('theme')

// Enable theme
// ...

More examples

Next.js middleware is handy for a number of functions, and I've only touched on the most basic of examples here! Check out the Vercel team's range of demos on GitHub, to see examples of authentication, i18n, A/B testing, and more.