NextJS - Lambda & S3

< NextJS Overview >

Next.js provides 4 types of rendering methods:

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
  (Static)  automatically rendered as static HTML (uses no initial props)
  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

1. Static Site Generation (SSG)

  • generates static resources like HTML at build time
  • The pre-rendered HTML is then reused on each request
  • a good pre-rendering strategy for static content that rarely changes
  • good for SEO

2. Server-Side Rendering (SSR)

  • pre-rendering method that generates the HTML at request time
  • when the application is already up and running
  • good for pages that are dynamic
  • also good for SEO since it is also a pre-render

3. Incremental Static Regeneration (ISR)

  • good for apps with many pages where build times are very high if purely using SSG
  • can build page per-page without needing to rebuild the entire app

4. Client-Side Rendering (CSR).

  • the typical rendering strategy where the application is rendered in the browser with JavaScript.

< Building NextJS app >

When a Next.js application is built, Next.js transforms the application to production-optimized files.

  • HTML for statically generated pages,
  • JavaScript for rendering on the server,
  • JavaScript for rendering on the client,
  • and CSS files.

< Deploying NextJS App to S3 Bucket >

Static Assets

  • Next.js uses the public directory under root to serve static assets such as images.
  • these files are best stored in persistent storage and backed by a content delivery network (CDN)
  • can add a prefix in the implementation to distinguish these static files.
  • Static files not stored in the public directory are in .next/static.
  • These static files are expected to be uploaded as _next/static to a CDN.
  • No other code in the .next/directory should be uploaded to a CDN because that would expose server code.
nextjs static export
const nextConfig = {
  reactStrictMode: true,
  output: 'export',
};
module.exports = nextConfig;

Static Site Generation (SSG) pages

  • For Static Site Generation, pages are rendered at build time and can be cached in CloudFront.
  • Clients connect to a CloudFront distribution, which is configured to forward requests for static resources to S3

Dynamic pages

  • API Gateway forwards requests to the Next.js app running on Lambda, which performs the server-side rendering.
  • To create dynamic routes, add brackets to a page file, for example, pages/user/[id].js
  • This creates a statically generated page with the path /user/[id] where [id] can be dynamic.

Output File Tracing

nextjs standalone export
const nextConfig = {
  reactStrictMode: true,
  output: 'standalone',
};
module.exports = nextConfig;
  • At build time, Next.js Output File Tracing determines the minimal set of files needed for deploying to Lambda.
  • The files are automatically copied to a standalone directory and must be enabled in next.config.js.

Github Actions

We can also create a github action yaml file to automatically upload build files to S3 everytime we push to main:

deploy.yml
# This workflow will do a clean installation of node dependencies, build and upload /out folder to Amazon S3
name: app-name
 
on:
  push:
    branches: [main]
 
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18.x'
          cache: 'npm'
      - name: Install dependencies
        run: npm ci
      - name: Build
        run: npm run build --if-present
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-southeast-1
      - name: Copy files to S3
        run: |
          aws s3 sync ./out s3://your-bucket-name/

< Deploying NextJS App to S3 >

Problem: Routing Issue

  1. 404 Error when access "http://bucket-name.s3-website-ap-southeast-1.amazonaws.com/portfolios" or "/portfolios/1"
  2. But User can access if we append .html to the path, e.g ".../portfolios.html"

Solution: Using CloudFront & Lambda@Edge

Solution is to create a CloudFront distribution and a Lambda@Edge to front the request to correct the path

1. Create a Lambda Function at us-east-1

  • We need to create the Lambda at us-east-1 as we are using it as Lambda@Edge.
  • Lambda@Edge functions are distributed globally, but they originate from one place.
  • The reason is most likely that there needs to be a single source of truth, and they picked us-east-1.
Create a Lambda Function
const hasExtension = /(.+)\.[a-zA-Z0-9]{2,5}$/;
const hasSlash = /\/$/;
 
export const handler = async (event) => {
  const { request } = event.Records[0].cf;
  const uri = request.uri;
  if (uri && !uri.match(hasExtension) && !uri.match(hasSlash)) {
    request.uri = `${uri}.html`;
  }
  return request;
};

2. Add "edgelambda.amazonaws.com" to Lambda Exection Role:

Go to IAM and find the lambda role and edit Trust Relationship:

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

3. Create a CloudFront distribution

  • After creating the Lambda, create a CloudFront Distribution
  • Under CloudFront Disribution behaviour tab, associate the lambda at Origin Request with Lambda@Edge Type.
  • Copy and paste the Lambda's ARN with the correct version.

< AWS Lambda Web Adapter [Github] >

  • Since the Next.js application is essentially a webserver, need to use the AWS Lambda Web Adapter
  • it serves as a Lambda layer to convert incoming events from API Gateway to HTTP requests that Next.js can process.
  • Once processed, the AWS Lambda Web Adapter converts the HTTP response back to a Lambda event response.
  • The Lambda handler is configured to run the minimal server.js file created by the standalone build step.

Sample Project

Go to the Sample Starter

In Windows
sam build --use-container
sam deploy --guided