Our experience adding Edge Runtime to Next.js SDK
Published: 2023-06-21 05:04:25

## Introduction

Edge Runtime has become a buzzword in the technology landscape, driving dynamic, low-latency functions in platforms from [AWS Lambda@Edge](https://aws.amazon.com/lambda/edge/) and [Cloudflare Workers](https://workers.cloudflare.com/) to [Vercel Edge](https://vercel.com/docs/concepts/functions/edge-functions). Emphasizing its importance, Vercel recently changed "experimental-edge" to "edge", signaling official support in their popular [Next.js](https://nextjs.org/) 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](https://nodejs.org/api/crypto.html) 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](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). Despite some minor differences, it's a solid stand-in for the crypto module. For instance, to generate random bytes:

```ts
// Node.js
crypto.randomFillSync(new Uint8Array(length);
// Edge Runtime
crypto.getRandomValues(new Uint8Array(length));
```

And hashing:

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

### 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](https://youmightnotneed.com/lodash) 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:

```ts
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;
  };
}
```

### Iron Session

The [iron-session](https://github.com/vvo/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:

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

### 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.

```ts
// 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;
```

### Redirecting

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

```ts
response.headers.append('Location', navigateUrl);
```

## 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.

```json
{
  "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"
    }
  }
}
```

## Wrapping up

You can check out the full source code of our Next.js SDK edge part [here](https://github.com/logto-io/js/tree/master/packages/next).

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.