Serverless GraphQL at the Edge

Posted on Feb 27th, 2020 in GraphQL

Serverless GraphQL API’s at the Edge. Did I hit on all of the buzzwords?

Modern applications have been moving more and more compute away from centralized servers and closer and closer to the edge. We introduced SPAs that can run on CDNs to be loaded onto our customers computers quickly. We introduced technology like lambda@edge to move compute closer out to the edge. And in today’s legal landscape, making decisions on network request routing closer to the edge makes even more sense. What does all of this have to do with GraphQL?

I'm getting there I promise
Joke Gif showing Micheal Scott skeptical of something

Why GraphQL at the Edge?

GraphQL is meant to be a query layer for you APIs. It is meant to be the layer between your frontend and your backend. It is supposed to describe your systems capabilities and represent your data in a way that allows you to query not just the data, but the relationships between that data as well. If GraphQL is truly the layer that combines backend capabilities and exposes them to frontend clients, then it makes sense to keep GraphQL as the gateway to your backend systems. If it is the gateway, why not move it closer to your customers? This provides a couple of benefits.

Told you
The aha moment

Caching

Caching in GraphQL is undoubtedly a real challenge. Retrieving data by submitting a POST request? That is not cacheable using the traditional model of request caching. But that is not to say that GraphQL is completely uncacheable. One of the largest themes of GraphQL Summit 2019 was caching, in particular "caching all the things’ ‘. A big performance gain comes from not only caching the full GraphQL response, but also the responses from all downstream providers. By leveraging GraphQL at the edge, you provide a mechanism to cache downstream services as close to your clients as you can, transforming your API layer into a sort of API CDN.

Privacy Laws Compliance

The biggest win gained from GraphQL at the edge is intelligent routing of user data and requests based on the location of the client request. If a client requests data from a country with strict data protection policies around data transfer, requests in that country can be routed to the closest servers which comply with those rules and regulations while providing a consistent API for the rest of the application. And because of the way GraphQL resolves data, part of your request (for example an email address) can be routed to compliant servers, while the rest of the request (like maybe content) can be resolved from anywhere and not necessarily the nearest server (but certainly try the nearest cache at least).

Why serverless GraphQL?

While the advantages of edge GraphQL should certainly make sense, why serverless? There are a few reasons. First, not managing any servers is great. Sure, the code still runs on a server somewhere, but you don’t have to manage that, and that should be enough of a selling point.

No managing servers?
Putin clapping and smiling because he just learned he doesn’t have to manage email servers

Secondly, cost. With serverless technologies, you pay for what you use. If you have a ton of traffic, then the cost may actually increase, but for most applications, lambdas are much cheaper than having a full application.

Finally, more and more CDN, edge, and cloud providers are providing more serverless capabilities at, well, the edge. AWS recently announced lambda@edge and Fastly is working to add Edge workers (they even built their own WebAssembly engine for this, but that’s a topic for another day). Pretty soon, the edge is going to be as feature rich as the rest of the cloud.

How to get started with Serverless GraphQL@edge

With lambda@edge in general availability, we’ll use the technology that we are already familiar with; AWS and lambda. Currently, lambda@edge only supports Node 12.X, Node 10.X and Python 3.7

In order to spin up a lambda@edge instance, you must create your lambda in us-east-1. On the create lambda screen, there is a place to select a blueprint. The blueprint needed is cloudfront-response-generation. Select that and then head over to the next screen to wrap up configuration. We now have the infrastructure basically planned out.

Writing the code

Note: I am still cleaning up the code so I don’t have any code on github yet

The code for this application is pretty simple. I simply leveraged apollo-server-express and express-serverless, but that is not enough. The events coming from cloudfront are a bit different than Lambda HTTP request events. So the first step is to reconcile those two. A CloudFront event looks a little like this:

  "Records": [
    {
      "cf": {
        "config": {
          "distributionDomainName": "d111111abcdef8.cloudfront.net",
          "distributionId": "EDFDVBD6EXAMPLE",
          "eventType": "viewer-request",
          "requestId": "4TyzHTaYWb1GX1qTfsHhEqV6HUDd_BzoBZnwfnvQc_1oF26ClkoUSEQ=="
        },
        "request": {
          "clientIp": "203.0.113.178",
          "headers": {
            "host": [
              {
                "key": "Host",
                "value": "d111111abcdef8.cloudfront.net"
              }
            ],
            "user-agent": [
              {
                "key": "User-Agent",
                "value": "curl/7.66.0"
              }
            ],
            "accept": [
              {
                "key": "accept",
                "value": "*/*"
              }
            ]
          },
          "method": "GET",
         "body": "...",
          "querystring": "",
          "uri": "/"
        }
      }
    }
  ]
}

While the LambdaHTTP event looks like:

{
  "body": "eyJ0ZXN0IjoiYm9keSJ9",
  "resource": "/{proxy+}",
  "path": "/",
  "httpMethod": "GET",
  "isBase64Encoded": true,
  "queryStringParameters": {
    "foo": "bar"
  },
  "multiValueQueryStringParameters": {
    "foo": [
      "bar"
    ]
  },
  "pathParameters": {
    "proxy": "/"
  },
  "stageVariables": {
    "baz": "qux"
  },
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate, sdch",
    "Accept-Language": "en-US,en;q=0.8",
    "Cache-Control": "max-age=0",
    "CloudFront-Forwarded-Proto": "https",
    "CloudFront-Is-Desktop-Viewer": "true",
    "CloudFront-Is-Mobile-Viewer": "false",
    "CloudFront-Is-SmartTV-Viewer": "false",
    "CloudFront-Is-Tablet-Viewer": "false",
    "CloudFront-Viewer-Country": "US",
    "Host": "1234567890.execute-api.us-east-2.amazonaws.com",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Custom User Agent String",
    "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
    "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
    "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
    "X-Forwarded-Port": "443",
    "X-Forwarded-Proto": "https"
  },
  "multiValueHeaders": {
    "Accept": [
      "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
    ],
    "Accept-Encoding": [
      "gzip, deflate, sdch"
    ],
    "Accept-Language": [
      "en-US,en;q=0.8"
    ],
    "Cache-Control": [
      "max-age=0"
    ],
    "CloudFront-Forwarded-Proto": [
      "https"
    ],
    "CloudFront-Is-Desktop-Viewer": [
      "true"
    ],
    "CloudFront-Is-Mobile-Viewer": [
      "false"
    ],
    "CloudFront-Is-SmartTV-Viewer": [
      "false"
    ],
    "CloudFront-Is-Tablet-Viewer": [
      "false"
    ],
    "CloudFront-Viewer-Country": [
      "US"
    ],
    "Host": [
      "0123456789.execute-api.us-east-2.amazonaws.com"
    ],
    "Upgrade-Insecure-Requests": [
      "1"
    ],
    "User-Agent": [
      "Custom User Agent String"
    ],
    "Via": [
      "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"
    ],
    "X-Amz-Cf-Id": [
      "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="
    ],
    "X-Forwarded-For": [
      "127.0.0.1, 127.0.0.2"
    ],
    "X-Forwarded-Port": [
      "443"
    ],
    "X-Forwarded-Proto": [
      "https"
    ]
  },
  "requestContext": {
    "accountId": "123456789012",
    "resourceId": "123456",
    "stage": "prod",
    "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
    "requestTime": "09/Apr/2015:12:34:56 +0000",
    "requestTimeEpoch": 1428582896000,
    "identity": {
      "cognitoIdentityPoolId": null,
      "accountId": null,
      "cognitoIdentityId": null,
      "caller": null,
      "accessKey": null,
      "sourceIp": "127.0.0.1",
      "cognitoAuthenticationType": null,
      "cognitoAuthenticationProvider": null,
      "userArn": null,
      "userAgent": "Custom User Agent String",
      "user": null
    },
    "path": "/prod/path/to/resource",
    "resourcePath": "/{proxy+}",
    "httpMethod": "POST",
    "apiId": "1234567890",
    "protocol": "HTTP/1.1"
  }
}

It’s not hard to translate the Cloudfront event to the vanilla Lambda event, but it does require some work. The Node Express Lambda wrapper looks something like this:

const server = awsServerlessExpress.createServer(app, undefined, binaryMimeTypes);
exports.handler = (event: any, context: any) => awsServerlessExpress.proxy(server, event, context);```

even for GraphQL APIs. We can see here that the function signature is the same as it would be for every single lambda. This means that we can just make a function that takes a CloudWatch event, and convert it to a Lambda event, and use the same exact code that we had before. The function looks like `const cfToLambda = (event: CFEvent) => LambdaEvent;` and then our handler just looks like:

const server = awsServerlessExpress.createServer(app, undefined, binaryMimeTypes);
exports.handler = (event: any, context: any) => awsServerlessExpress.proxy(server, cdToLambda(event), context);



That's really it for the code.

## Gotchas

There are a few Gotchas when working with edge Lambdas. Firstly, it can take _forever_ to provision and distribute your lambda. The reason is because CloudFront needs to work to distribute and replicate your lambdas across the CDN network. Secondly, Deleting an edge Lambda is not like deleting any other lambda. You need to [follow these instructions](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-delete-replicas.html) to delete edge lambdas.

The biggest gotchas are around the request times and limits. Unlike traditional lambdas, you have a more restrictive limits. Your lambda has a timeout of 30 seconds on origin request and response events and  5 seconds on viewer request and response event. Your body size is also limited to 1MB and 40KB respectively. [You can see more restrictions here.](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html#lambda-requirements-see-limits).

## Final Notes

I haven't used this in production yet, this is completely theoretical. If you are using this in production, I would love to hear more. There are considerations around caching and performance that I didn't cover. Lambda@edge has access to see what the closest AWS region via an environment variable (process.env.AWS_REGION). This may not be useful at all, but it's certainly fun to think about!

Read More Related Posts

Read Latest Posts

Server Side State management in NextJS: a deep dive into React Cache

Posted in NextJS on Oct 18th, 2024

In previous posts, I explored how Server Components in NextJS improve performance by offloading more rendering to the server. Managing server-side state differs from client-side, as it’s static and immutable during a single render. NextJS provides tools like cache, unstable_cache, and patched fetch to manage this state efficiently, reducing prop drilling and simplifying code. These tools offer caching across requests, but require a shift from traditional client-side approaches for better performance

Read More->

NextJS foot guns: Over-reliance on Client Side State

Posted in NextJS on Oct 10th, 2024

This article builds on “Architecting NextJS Applications for Better Performance,” focusing on optimizing state management in NextJS projects. It explores the pitfalls of overusing Context providers and client-side state, which can bloat bundles and hurt performance. By strategically identifying which parts of your app should be client or server components, you can leverage NextJS’s capabilities to build faster, more efficient applications.

Read More->

Architecting NextJS Applications for Better Performance

Posted in JAM Stack on Oct 2nd, 2024

Explore how to optimize React 18 applications using Next.js for better performance and SEO. It covers the use of Server Components to reduce JavaScript on the client, improve Core Web Vitals (CWV), and enhance User Experience (UX). Key strategies include isolating client components, minimizing their size, and balancing client-server rendering. It also highlights Next.js’s render lifecycle and provides tips for building faster, SEO-friendly applications.

Read More->

Authorization in GraphQL

Posted in GraphQL on Jun 8th, 2020

GraphQL is a great new technology. It fills many gaps the REST leaves. However, there are challengs with GraphQL. One of those challenges are authorization and authentication.

Read More->