Skip to content
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
example.com/swede-feels
I love Sweden!/pages/swede-love/index.js
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 place a file named _middleware.js within the directory in which you'd like it to run:

/pages/_middleware.js
import { NextResponse } from 'next/server'
	
export function middleware (request) {
  return NextResponse.next()
}

This function is in /pages so it 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, the page will load as expected, as if there's no middleware:

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

Return a response

Returning a Response (MDN) from the function will return the result from the response, instead of the Next.js page:

/pages/_middleware.js
export function middleware (request) {
  return new Response('Hello world!')
}
An diagram of a web browser displaying "hello world"
example.com
Hello world!

Other types of Response can be returned too, such as JSON, Blobs, Streams etc. 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 directing from /2019 to /2022.

An interactive diagram of a website redirecting to another URL
example.com/2019
Welcome to 2022!/pages/2022/index.js
DevTools

This _middleware function is placed with the /pages/2019 folder so that only connections to this directory are affected.

/pages/2019/_middleware.js
import { NextResponse } from 'next/server'
	
export function middleware () {
  // Beware loops when redirecting to the same directory
  return NextResponse.redirect('/2022')
}

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
beta.example.com/start
Start with the beta./pages/beta/start.js
DevTools

Here we check for visits on the hostname beta.example.com, and then serve example.com/beta instead. We're adding pathname to the end of the URL to make sure that all beta pages redirect, not just the root.

/pages/_middleware.js
import { NextResponse } from 'next/server'
	
export function middleware (request) {
  const hostname = request.headers.get('host')
  const { pathname } = request.nextUrl
	
  if (hostname === 'beta.example.com') {
    return NextResponse.rewrite(`/beta${pathname}`)
  }
}

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
example.com/api/query
{ success: true }/pages/api/query.js
DevTools

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

/pages/api/_middleware.js
import { NextResponse } from 'next/server'
	
const secretKey = 'artichoke'
	
export function middleware (request) {
  const headerKey = request.headers.get('secret-key')
	
  if (headerKey === secretKey) {
    return NextResponse.next()
  }
	
  return new Response(null, { status: 401 })
}

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
example.com/counter
View count is 571/pages/counter.js
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.

/pages/counter/_middleware.js
export function middleware (request, event) {
  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 (I use Upstash)
      // ...
    })()
  )
}

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 page, unless a Danish user connects, where we'll serve a blank 403 error page instead.

An interactive diagram of a web browser demonstrating a being blocked in Denmark, but not in Belgium and Czechia
example.com/swede-feels
I love Sweden!/pages/swede-love/index.js
DevTools

The request.geo object contains the two-letter country code we need:

/pages/swede-feels/_middleware.js
import { NextResponse } from 'next/server'
	
export function middleware (request) {
  // request.geo.country is undefined in dev mode, 'US' as backup
  const country = request.geo.country || 'US'
	
  // If visited from Denmark
  if (country === 'DK') {
    return new Response(null, { status: 403 })
  }
}

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
example.com
View my articles./pages/articles.js
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.

/pages/_middleware.js
import { NextResponse } from 'next/server'
import SunCalc from 'suncalc'
	
export function middleware (request) {
  // Skip if theme already set
  if (request.cookies.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
  return NextResponse.next().cookie('theme', mode)
}

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.