Traditionally there are two ways to serve content, statically from a Content Delivery Network (CDN) close to the user for fast response times, or dynamically, with personalization configured at the server level on each request.

When deciding on how you want to deliver content to your applications visitors, you have to take into consideration the trade-offs that each of these two options offer.

A static page will deliver the same content to all visitors, no matter where they are in the world, and it will be fast as it's cached by the CDN. But this approach may not be viable if you want to deliver personalized content, depending on, for example, where a user is located in the world.

To give your user a personalized experience, you can take advantage of server-side rendering to create dynamic content on each request to your sites pages. This will enable you to offer different content to people based on their location, authenticate them, or configure the language of your site.

The draw back of this approach is that it can be slower. If the server processing the request is far away from the visitors origin, then the request can take time to complete, and the content may not be available to the user at the speed offered by serving purely static content.

What are Edge Functions?

To achieve both speed and dynamism, you can use Edge Functions. They allow you to deliver content to your sites visitors with speed and personalization, are deployed globally by default on Vercel's Edge Network, and have zero cold starts. They enable you to move server-side logic to the Edge, close to your visitors origin.

To use Edge Functions, you can deploy Middleware. Middleware is code that executes before a request is processed. Depending on the incoming request, you can execute custom logic, rewrite, redirect, add headers and more, before returning a response.

Middleware

The middleware function runs code before a request is completed, then based on the request, you can modify the response. It can be used for anything that shares logic between pages.

It takes two parameters, request and event. The request parameter is an extension of the native Request interface and has added methods and properties that include accessing cookies, getting geo location from an IP Address and user-agent info. You can import its type definition with NextRequest

In addition you can import the NextResponse API, which extends the native Response interface and lets you redirect, rewrite, cookies, and clear cookies.

Middleware use-cases

Links to example projects which show how to use each Middleware use-case listed

How to use Middleware

To start using Middleware in your Next.js project, begin by upgrading to the latest Next.js version. The following steps will guide you through the process. Note that the below example uses TypeScript, though this is not a requirement.

  1. Install the latest version of next:

    npm install next@latest
    # or
    yarn upgrade next@latest
    
  2. Next, create a _middleware.ts file under your /pages directory.

    - /pages
      _middleware.ts
    - package.json
    
  3. Finally create function in the _middleware.ts file.

    export default function middleware(req, ev) {
      return new Response('Hello, world!');
    }
    

When you deploy your site, your Middleware will work out of the box.

API

Middleware is created by using a middleware function that lives inside a _middleware file. Its API is based upon the native FetchEvent, Response, and Request objects.

These native Web API objects are extended to give you more control over how you manipulate and configure a response, based on the incoming requests.

The function signature:

import type { NextFetchEvent } from 'next/server';
import type { NextRequest } from 'next/server';

export type Middleware = (
  request: NextRequest,
  event: NextFetchEvent,
) => Promise<Response | undefined> | Response | undefined;

The function can be a default export and as such, does not have to be named middleware. Though this is a convention. Also note that you only need to make the function async if you are running asynchronous code.

Warning: Edge Functions are currently in Beta. The API might change as we look to continually make improvements.

NextFetchEvent

The NextFetchEvent object extends the native FetchEvent object, and includes the waitUntil() method.

The waitUntil() method can be used to prolong the execution of the function, after the response has been sent. In practice this means that you can send a response, then continue the function execution if you have other background work to make.

The following example creates a stream and sends the response async:

import type { NextFetchEvent, NextRequest } from 'next/server';

export async function middleware(req: NextRequest, event: NextFetchEvent) {
  if (req.nextUrl.pathname === '/responses/send-response') {
    const { readable, writable } = new TransformStream();

    event.waitUntil(
      (async () => {
        const writer = writable.getWriter();
        const encoder = new TextEncoder();
        writer.write(encoder.encode('Hello, world! Streamed!'));
        writer.write(encoder.encode('response'));
        writer.close();
      })(),
    );

    return new Response(readable);
  }
}

Another example of why you would use waitUntil() is integrations with logging tools such as Sentry or DataDog. After the response has been sent, you can send logs of response times, errors, API call durations or overall performance metrics.

The event object is fully typed and can be imported from next/server.

import { NextFetchEvent } from 'next/server';

NextRequest

The NextRequest object is an extension of the native Request interface, with the following added methods and properties:

  • cookies - Has the cookies from the Request
  • nextUrl - Includes an extended, parsed, URL object that gives you access to Next.js specific properties such as pathname, basePath, trailingSlash and i18n
  • geo - Has the geo location from the Request
    • geo.country - The country code
    • geo.region - The region code
    • geo.city - The city
  • ip - Has the IP address of the Request
  • ua - Has the user agent
Note: When working locally, your IP address will be 127.0.0.1. This means that thegeo location can't be computed and you will see an empty object. To check the geo objects properties, push your code and Vercel will create a deployment with a Preview URL, this URL can be used to check the geo location values from the Request.

You can use the NextRequest object as a direct replacement for the native Request interface, giving you more control over how you manipulate the request.

NextRequest is fully typed and can be imported from next/server:

import type { NextRequest } from 'next/server';

Example using the geo object to check a requests location and blocking if it does not match an allowlist:

import type { NextRequest } from 'next/server';

// Block GB, prefer US
const BLOCKED_COUNTRY = 'GB';

export function middleware(req: NextRequest) {
  const country = req.geo.country || 'US';

  // If the request is from the blocked country,
  // send back a response with a status code
  if (country === BLOCKED_COUNTRY) {
    return new Response('Blocked for legal reasons', { status: 451 });
  }

  // Otherwise, send a response with the country
  return new Response(`Greetings from ${country}, where you are not blocked.`);
}

NextResponse

The NextResponse object is an extension of the native Response interface, with the following added methods and properties:

  • cookies - An object with the cookies in the Response
  • redirect() - Returns a NextResponse with a redirect set
  • rewrite() - Returns a NextResponse with a rewrite set
  • next() - Returns a NextResponse that will continue the middleware chain

All methods above return a NextResponse object that only takes effect if it's returned in the middleware function.

NextResponse is fully typed and can be imported from next/server.

import { NextResponse } from 'next/server';

Example using rewrite() to rewrite the response to a different URL based on the request (browser) location:

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

export function middleware(req: NextRequest) {
  const country = req.geo.country?.toLowerCase() || 'us';

  req.nextUrl.pathname = `/${country}`;
  return NextResponse.rewrite(req.nextUrl);
}

Why does redirect() use 307 and 308?

When using redirect() you may notice that the status codes used are 307 for a temporary redirect, and 308 for a permanent redirect. While traditionally a 302 was used for a temporary redirect, and a 301 for a permanent redirect, many browsers changed the request method of the redirect, from a POST to GET request when using a 302, regardless of the origins request method.

Taking the following example of a redirect from /users to /people, if you make a POST request to /users to create a new user, and are conforming to a 302 temporary redirect, the request method will be changed from a POST to a GET request. This doesn't make sense, as to create a new user, you should be making a POST request to /people, and not a GET request.

The introduction of the 307 status code means that the request method is preserved as POST.

  • 302 - Temporary redirect, will change the request method from POST to GET
  • 307 - Temporary redirect, will preserve the request method as POST

The redirect() method uses a 307 by default, instead of a 302 temporary redirect, meaning your requests will always be preserved as POST requests.

Middleware running order

If you do not have any sub-directories, the Middleware will run on all routes within the /pages directory and public files like /favicon.ico. The below example assumes you have about.ts and teams.ts routes.

- /pages
  _middleware.ts # Will run on all routes under /pages
  index.ts
  about.ts
  teams.ts
- package.json

If you do have sub-directories with nested routes, the Middleware will run in a top-down fashion. For example, if you have created /pages/about/_middleware.ts and /pages/about/team/_middleware.ts, the Middleware will run first on /pages/about, and then /pages/about/team. The below example shows how this works with a nested routing structure.

- /pages
  index.ts
  - /about
    _middleware.ts # Will run first
    about.ts
    - /teams
      _middleware.ts # Will run second
      teams.ts
- package.json

Runtime

Once the Middleware is deployed, it will run within a V8 Runtime with a limited set of APIs. In development the code will run in a sandbox environment that emulates the production runtime.

Because of this, there are some restrictions to writing Middleware. These include:

  • Native Node.js APIs are not supported. For example, you can't read or write to the filesystem
  • Node Modules can be used, as long as they implement ES Modules and do not use any native Node.js APIs. For example, you could use the path-to-regexp package to do path matches
  • You can use ES Modules and split your code into reusable files that will then be bundled together when the application is built
  • Calling require directly is not allowed. If you do use it, it might work when the import path can be statically resolved, but it is not recommended. Use ES Modules instead

Runtime APIs

The following objects and APIs are available in the runtime:

Globals

Base64

  • atob: Decodes a string of data which has been encoded using base-64 encoding
  • btoa: Creates a base-64 encoded ASCII string from a string of binary data

Encoding

  • TextEncoder: Takes a stream of code points as input and emits a stream of bytes (UTF8)
  • TextDecoder: Takes a stream of bytes as input and emit a stream of code points

Environment

  • process.env: Holds an object with all environment variables used in Middleware, for both production and development environments. If you are not using any environment variables in your Middleware, the process.env object will be empty. process.env will only include environment variables actually used.

Fetch

The Web Fetch API can be used from the runtime, enabling you to use Middleware as a proxy, or connect to external storage APIs

A potential caveat to using the Fetch API in a Middleware function is latency. For example, if you have a Middleware function running a fetch request to New York, and a user accesses your site from London, the request will be resolved from the nearest Edge to the user (in this case, London), to the origin of the request, New York. There is a risk this could happen on every request, making your site slow to respond. When using the Fetch API, you must make sure it does not run on every single request made.

Streams

  • TransformStream: Consists of a pair of streams: a writable stream known as its writable side, and a readable stream, known as its readable side. Writes to the writable side, result in new data being made available for reading from the readable side. Support for web streams is quite limited at the moment, although it is more extended in the development environment
  • ReadableStream: A readable stream of byte data
  • WritableStream: A standard abstraction for writing streaming data to a destination, known as a sink

Timers

  • setInterval: Schedules a function to execute every time a given number of milliseconds elapses
  • clearInterval: Cancels the repeated execution set using setInterval()
  • setTimeout: Schedules a function to execute in a given amount of time
  • clearTimeout: Cancels the delayed execution set using setTimeout()

Web

Crypto

  • Crypto: The Crypto interface represents basic cryptography features available in the current context
  • crypto.randomUUID: Lets you generate a v4 UUID using a cryptographically secure random number generator
  • crypto.getRandomValues: Lets you get cryptographically strong random values
  • crypto.subtle: A read-only property that returns a SubtleCrypto which can then be used to perform low-level cryptographic operations

Logging

  • console.debug: Outputs a message to the console with the log level debug
  • console.info: Informative logging of information. You may use string substitution and additional arguments with this method
  • console.clear: Clears the console
  • console.dir: Displays an interactive listing of the properties of a specified JavaScript object
  • console.count: Log the number of times this line has been called with the given label
  • console.time: Starts a timer with a name specified as an input parameter

Unsupported APIs

The Edge Runtime has some restrictions including:

  • Native Node.js APIs are not supported. For example, you can't read or write to the filesystem
  • Node Modules can be used, as long as they implement ES Modules and do not use any native Node.js APIs
  • Calling require directly is not allowed. Use ES Modules instead

The following JavaScript language features are disabled, and will not work:

  • eval: Evaluates JavaScript code represented as a string
  • new Function(evalString): Creates a new function with the code provided as an argument

The following Web APIs are currently not supported, but will be in the future:

Technical Details

Maximum Execution Duration

The maximum duration for an Edge Function execution is 30 seconds, but the function needs to return a response in less than 1.5 seconds, otherwise the request will time out.

This means that you should return a response as soon as possible, and continue with any asynchronous workloads in the background, after returning the response.

Code size limit

The maximum size for an Edge Function is 1 MB, including all the code that is bundled in the function.

If you reach the limit, make sure the code you are importing in your function is used and is not too heavy. You can use a package size checker tool like bundle to check the size of a package and search for a smaller alternative.