How I Used Lambda Edge Functions to Deploy a Next.js App on S3

There were some hurdles while deploying my blog to S3, but AWS Lambda Edge functions came to the rescue

July 17, 20233 min readAWS


For the most part, setting up this blog has been a pretty straightforward process. I used a Next.js template to get started, wrote some blog posts, and wrote a script to build the site and upload the files to an S3 bucket. I then registered a domain name and set up a Cloudfront distribution to direct traffic from my domain name to the S3 bucket.

Everything was working great, but then I noticed something - when I went to one of my posts and refreshed the page, I got a 404 Not Found error!

This was unacceptable - I wanted to be able to share links to specific posts, and this made it impossible to do that.

What was happening?

When I'm on the home page of microthoughts.dev, the root index.html file is served up. When I click on one of the blog posts on the home page, I am routed to /posts/post-name. This happens on the client side, so everything works great. However, when I refresh the page, Cloudfront is looking for a file in my S3 bucket at the path /posts/post-name, which does't exist and so an error is thrown.

However, Next.js generates an HTML file for every one of my posts, for example /posts/post-name.html.

After asking ChatGPT for some help, it suggested creating a Lambda Edge function that takes a Cloudfront view request as a trigger and routes the request to the appropriate HTML file in the S3 bucket.

Creating the edge function

I first created a Lambda function with a runtime of Node JS 18.x. Then, I added this code that ChatGPT wrote:

export const handler = async (event, context, callback) => {
  const request = event.Records[0].cf.request
  const { uri } = request

  // Check if the request path does not end with ".html" and does not have a file extension
  if (!uri.endsWith(".html") && !uri.includes(".")) {
    // Append ".html" to the path
    request.uri = `${uri}.html`
  }

  // If the requested path is "/404" or ends with "/404", redirect to "/404.html"
  if (uri === "/404" || uri.endsWith("/404")) {
    request.uri = "/404.html"
  }

  callback(null, request)
}

I created the function, and then in the IAM console, I needed to edit the role for the lambda function. I added this trust policy in the "Trust relationships" section:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "edgelambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    },
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Then, I went back to the lambda function and added a cloudfront trigger for the distribution I had set up for "Viewer requests". I deployed the function and tested out refreshing a blog post on my site. Viola! It worked like magic. The page refreshed as expected.

Conclusion

You can use this edge function for any app that has client side routing! It doesn't just have to be a Next app or even a React app, as long as the route you are redirecting to has a corresponding HTML file in your S3 bucket.

Good luck!