Migrating to the modern Netlify Functions

by Eduardo Bouças

Did you know that we’ve rebuilt Netlify Functions from the ground up, with a reimagined experience and packed to the brim with new features? If you’re still using the legacy version, you’re missing out! This guide will help you migrate in a few simple steps.

#Some quick history

It’s been a while since we first launched Netlify Functions. For the first time, developers could augment their frontend applications with backend functionality without leaving the comfort and convenience of the Netlify experience they knew and loved.

Back then, AWS Lambda was the state-of-the-art solution in the serverless computing space, so Netlify Functions launched with a fully compatible API surface, making it easy for developers to port over any existing workflows.

Functions quickly became an incredibly popular feature of our platform, used by millions of developers on a wildly diverse set of use cases.

Late last year, we took everything we learned over the course of five years and released a major update to the product, with a redesigned API based on web standards and a set of features that unlock an entire new category of use cases.

We kept the legacy version around so that developers could migrate at their own pace, but we haven’t updated it with any new features and we’re going to start deprecating it 2025.

#What’s new

Before I ask you to change any code, let me try to convince you that it’s worth your trouble. Here’s what you’ll get out of the box when you start using the modern Netlify Functions.

  • Built on web standards: The new Netlify Functions API has a really simple design built around web platform standards: a function receives a Request and returns a Response. So rather than having to learn a proprietary API, your team can just learn the web platform.
  • Configurable URL: Gone are the days when setting the URL of your function involved creating a rewrite rule to the default /.netlify/functions/<name> endpoint. You can now configure the URL right from the function code.
  • Advanced routing: To give you better control over when your function should or should not run, you can set it to respond only to certain HTTP methods and provide a list of exclusion paths. You can also build complex routing logic by defining path parameters using the URLPattern standard.
  • Zero-config streaming: If you want to use response streaming to start getting data in front of your users as quickly as possible, your function can simply return a stream. There’s no need to annotate your code or configure anything.
  • Zero-config Blobs: Use Netlify Blobs within your function with no setup required. To interact with a data store, import the @netlify/blobs module and call the methods to read and write data as you please.
  • Static file shadowing: When building an application that leverages a combination of static and dynamic data, you can configure your endpoint to serve a static file if one exists at that path and only invoke the function if not.
  • Rate limiting: You can define custom rate-limiting rules for your endpoints right from your function code.
  • Simple cookies: Manipulate cookies with ease by using type-safe, convenient utility methods based on the CookieStore standard.
  • Geolocation data: Unlock personalisation use cases with detailed information about your visitors, like their IP address, country, city, timezone and even postal code.
  • Netlify metadata: Get easy access to metadata about your Netlify property, including information about your account, site and deploy.
  • Global Netlify object: Access any of the features listed above (and more) from anywhere in your function, even in deeply-nested files or modules that don’t have access to the handler scope.

#Code changes

«Okay, but I have a lot of functions using the old syntax and I don’t want to risk breaking my site with complex migrations». I hear you!

First of all, Netlify gives you different tools that make this type of migration safe:

  • You can use the Netlify CLI to run your functions locally before deploying
  • You can use deploy previews to validate your changes before publishing to production
  • The @netlify/function package exposes types for all the APIs, giving TypeScript users full type-safety

You do need to make some changes to your code, but they’re limited to a small set of areas. Let’s get started.

#Move to ES Modules

One of the key pillars of the modern Netlify Functions is the idea of embracing the web platform and building with standard APIs whenever possible. This includes the format in which function files are defined.

The modern Netlify Functions work exclusively with the standard ECMAScript modules format (also known as ES Modules or ESM). The CommonJS format is not supported, since it comes with a set of drawbacks and it was never standardised beyond its Node.js implementation.

To move to ESM, start by changing the way you import and export data:

  • Imports with require() must be changed to import declarations
  • Imports of local files must always include the file extension
  • Exports with module.exports must be changed to export declarations
const { purgeCache } = require("@netlify/functions");
import { purgeCache } from "@netlify/functions";
const util = require("./lib/file");
import util from "./lib/file.js";
module.exports.handler = async (event, context) => {
export async function handler(event, context) {
return { body: "I'm a teapot", statusCode: 418 };
}

If you use VS Code, you can get it to handle this transformation for you: hover next to the require keyword, click on “Quick fix” and select “Convert to ES Module”.

Automating the CJS->ESM migration in VS Code

Finally, you must instruct Node.js to treat your function as an ESM file by doing at least one of the following:

  • Rename the function’s entry file to use the .mjs extension (or .mts if you want to use TypeScript)
  • Have a package.json file at the root of your project with the type property set to module

There are a few other changes that may affect you, depending on how your function is structured:

import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
console.log("Current file:", __filename);
console.log("Current file:", fileToURLPath(import.meta.url));
console.log("Current directory:", __dirname);
console.log("Current directory:", dirname(fileToURLPath(import.meta.url)));

#Change the handler signature

In the modern Netlify Functions, the function handler is declared using a default export and not the named handler export.

Another notable change is in the signature of the handler. Instead of receiving a Lambda event, your function will receive a Request and a context object. Equally, rather than returning a Lambda response object, your function must return a Response.

This has implications on how you perform operations like accessing the request URL, reading and setting headers or defining the response status code.

export async function handler(event, context) {
export default async function (request, context) {
const url = event.rawUrl;
const url = request.url;
const ua = event.headers["user-agent"];
const ua = request.headers.get("user-agent");
console.log(`Handling request for ${url} with user agent ${ua}`);
return { body: "I'm a teapot", statusCode: 418 };
return new Response("I'm a teapot", { status: 418 });
}

If you use scheduled functions, you must declare the cron expression in a schedule configuration property instead of using a wrapper function. Also, the handler no longer needs to return anything, since there’s no client waiting for a response.

import { schedule } from "@netlify/functions";
async function myTask(event, context) {
export default async function(request, context) {
const { next_run } = JSON.parse(event.body);
const { next_run } = await request.json();
console.log("BOOP! Next run:", next_run);
return { body: "", statusCode: 200 };
}
export const handler = schedule("@daily", myTask);
export const config = { schedule: "@daily" };

When you start using the new API, you’ll notice that you’re working mostly with web standards and I think you’ll love how intuitive and predictable that feels — as much as I like the Netlify documentation, nothing beats being able to just refer to the documentation of the web platform.

#Supercharge your functions

Now that your function is using the updated syntax, let’s look at how you can leverage some of the powerful features it gets you out of the box. In the example below, we’ll configure the function URL with a path parameter and set it to run only on GET requests.

import type { Config, Context } from "@netlify/functions";
export const config: Config = {
// Configuring the function to only run on GET requests.
method: "GET",
// Setting the URL path with a parameter. This means it will
// match `/products/keyboard` and `/products/mouse`, exposing
// the strings "keyboard" and "mouse" in `context.params.name`.
path: "/products/:name",
};
export default async function (req: Request, context: Context) {
// Accessing `context.geo` to make decisions based on the
// user's location.
if (context.geo.country.code !== "us") {
return new Response("Sorry, not available", { status: 403 });
}
// Reading a cookie with `context.cookies`.
const userID = context.cookies.get("uid");
// Extracting the `name` parameter from the URL.
const productName = context.params.name;
// Making an HTTP call to a third-party API. If we don't
// need to process the response within the function, we
// can return it directly which will automatically enable
// response streaming.
return await fetch(`https://example.com/api/${productName}`, {
headers: {
"x-user-id": userID,
},
});
}

And that’s it! Your function has gained superpowers with just a few changes.

#Bonus: two for one

Now that you’ve familiarised yourself with the modern Netlify Functions, you might like to know that you also learned how to use another serverless compute primitive in the process: Netlify Edge Functions.

Even though they run on different JavaScript runtimes, they have identical APIs and configuration options. In fact, you can try moving your function file to the netlify/edge-functions directory and it should just work.