DEV Community

Wang Sijie
Wang Sijie

Posted on • Originally published at blog.logto.io

Our experience adding Edge Runtime to Next.js SDK

Introduction

Edge Runtime has become a buzzword in the technology landscape, driving dynamic, low-latency functions in platforms from AWS Lambda@Edge and Cloudflare Workers to Vercel Edge. Emphasizing its importance, Vercel recently changed "experimental-edge" to "edge", signaling official support in their popular Next.js framework.

With our Next.js SDK gaining serious traction, we at Logto thought, "Why not add Edge Runtime support?" So, we rolled up our sleeves and jumped right in. In this article, we're going to share our adventure, looking at the hurdles we faced, how we overcame them, and the cool stuff we learned along the way.

Transitioning modules and dependencies for Edge Runtime support

Working with Edge Runtime poses some unique challenges, primarily because it doesn't support all modules and dependencies commonly used in Node.js. We ran into this issue with the crypto, lodash, and iron-session modules, necessitating some innovative workarounds.

Crypto

In a Node.js environment, the crypto module serves as a wrapper for OpenSSL cryptographic functions. Unfortunately, Edge Runtime doesn't support it. But don't fret - most Edge Runtimes come to the rescue with support for the Web Crypto API. Despite some minor differences, it's a solid stand-in for the crypto module. For instance, to generate random bytes:

// Node.js
crypto.randomFillSync(new Uint8Array(length);
// Edge Runtime
crypto.getRandomValues(new Uint8Array(length));
Enter fullscreen mode Exit fullscreen mode

And hashing:

// Node.js
const hash = createHash('sha256');
hash.update(encodedCodeVerifier);
const codeChallenge = hash.digest();
// Edge Runtime
const codeChallenge = await crypto.subtle.digest('SHA-256', encodedCodeVerifier);
Enter fullscreen mode Exit fullscreen mode

Lodash

Lodash is a favorite among many developers for its utility, but Edge Runtime isn't a fan. Our workaround? We swapped out Lodash functions with native JavaScript methods, keeping our code both efficient and readable.

While replacing most Lodash functions wasn't a Herculean task, it did require some finesse. Let's take a peek at how we recreated the utility of "once" in our own way:

type Procedure<T> = (...args: unknown[]) => T;

export function once<T>(function_: Procedure<T>): Procedure<T> {
  let called = false;
  let result: T;

  return function (this: unknown, ...args: unknown[]) {
    if (!called) {
      called = true;
      result = function_.apply(this, args);
    }

    return result;
  };
}
Enter fullscreen mode Exit fullscreen mode

Iron Session

The iron-session module's latest version is Edge Runtime-friendly, so all we had to do was update our version. Simple as that!

Navigating the Intricacies of "Response" in Edge Runtime

Another challenge that we faced when adapting our SDK for Edge Runtime was handling the differences in the "Response" object. Here's how we overcame these differences:

Creating a response manually

Unlike in Node.js, a request in Edge Runtime doesn't come with a comming request. This meant that we had to create it by calling new Response(), here is an example of returning data:

return new Response(JSON.stringify(context), {
  status: 200,
  headers: {
    'content-type': 'application/json',
  },
});
Enter fullscreen mode Exit fullscreen mode

Letting go of "withIronSessionApiRoute"

In the Edge Runtime, the Response.body is a read-only affair. This means that we couldn't initialize a response before the data was prepared. As a result, our trusty "withIronSessionApiRoute" (along with other middleware) had to be benched.

To understand what we replaced, let's first unpack what withIronSessionApiRoute actually does:

  1. It takes a peek at the cookie, constructs a session object, and ties it to res.
  2. It automatically appends the "set-cookie" header to res if there's a change in the session.

So, how did we emulate this functionality in our new Edge Runtime setting?

  1. Read: We utilized the existing getIronSession function. By giving it an empty and fake response, retrieves the session as needed. This replaced the "get" method from req.session.
  2. Writing: We prepared a response with data upfront, then used getIronSession on this response instance to obtain the session object. Once we had this object in our hands, we could modify the session as required.
// Read
const getLogtoContext = async (request: NextRequest) => {
  const session = await getIronSession(request, new Response());
  const context = await this.getLogtoUserFromRequest(session);

  return context;
};

// Write
const response = new Response(JSON.stringify(user), {
  status: 200,
});
const session = await getIronSession(request, response);

// Modify session
session.userId = 'foo';

return response;
Enter fullscreen mode Exit fullscreen mode

Redirecting

Redirection in Edge Runtime required us to manually add a Location header to our responses.

response.headers.append('Location', navigateUrl);
Enter fullscreen mode Exit fullscreen mode

One package, two runtimes

In this journey of ours, we decided to stick to a single package to support both Edge and Node.js runtimes.

Here’s why

We thought about creating a separate package for Edge, but quickly realized it was unnecessary. Most of our code was shared between the two runtimes, with only a handful of lines needing tweaks. Plus, using the SDK remains pretty much the same across both runtimes, so maintaining a unified package made the most sense.

Here's what we did

Instead of duplicating efforts, we decided to expand the existing package. We added an "edge" folder right in the package's root, cozying up next to the old "src" folder. Then, we updated the package.json file, adding a new path to the "exports". This way, both Edge and Node.js runtimes could live harmoniously within the same package, with minimal fuss.

{
  "exports": {
    ".": {
      "require": "./lib/src/index.js",
      "import": "./lib/src/index.mjs",
      "types": "./lib/src/index.d.ts"
    },
    "./edge": {
      "require": "./lib/edge/index.js",
      "import": "./lib/edge/index.mjs",
      "types": "./lib/edge/index.d.ts"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Wrapping up

You can check out the full source code of our Next.js SDK edge part here.

By sharing our journey of embrace Edge Runtime, we hope to inspire and guide others exploring similar paths. Stay tuned for more updates with our Next.js SDK.

Top comments (0)