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));
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);
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;
};
}
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',
},
});
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:
- It takes a peek at the cookie, constructs a session object, and ties it to
res
. - 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?
-
Read: We utilized the existing
getIronSession
function. By giving it an empty and fakeresponse
, retrieves the session as needed. This replaced the "get" method fromreq.session
. -
Writing: We prepared a
response
with data upfront, then usedgetIronSession
on thisresponse
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;
Redirecting
Redirection in Edge Runtime required us to manually add a Location
header to our responses.
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.
{
"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.
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)