Edge Functions#
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 application 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 drawback 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 (Vercel & Nextjs)#
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 geolocation 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#
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.
-
Install the latest version of next
:
npm install next@latest
# or
yarn upgrade next@latest
-
Next, create a _middleware.ts
file under your /pages
directory.
- /pages
_middleware.ts
- package.json
-
Finally, create function in the _middleware.ts
file.
export default function middleware(req, ev) {
return new Response({
body: '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.
An 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
geo.latitude
– The latitude
geo.longitude
– The longitude
ip
– Has the IP address of the Request
ua
– Has the user agent
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.`);
}
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:
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
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
process.env
: Holds an object with all environment variables for both production and development in the exact same way as any other page or API in Next.js
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.
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
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()
Headers
: A WHATWG implementation of the headers API
URL
: A WHATWG implementation of the URL API.
URLSearchParams
: A WHATWG implementation of URLSearchParams
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
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
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 a bundle to check the size of a package and search for a smaller alternative.