Skip to main content

NextjsSite

The NextjsSite construct is a higher level CDK construct that makes it easy to create a Next.js app. It uses the OpenNext project to build your Next.js app, and transforms the build output to a format that can be deployed to AWS. The OpenNext project is maintained by the SST team🧑.

The NextjsSite construct provides a simple way to build and deploy the app to AWS:

  • The client assets are deployed to an S3 Bucket, and served out from a CloudFront CDN for fast content delivery.
  • The app server and API functions are deployed to Lambda. And the middleware functions are deployed to Lambda@Edge.
  • It enables you to configure custom domains for the website URL.
  • It also enable you to automatically set the environment variables for your Next.js app directly from the outputs in your SST app.
  • It provides a simple interface to grant permissions for your app to access AWS resources.

Quick Start​

  1. If you are creating a new Next.js app, run create-next-app from the root of your SST app.
npx create-next-app@latest

Create Next.js App template

After the Next.js app is created, your SST app structure should look like:

my-sst-app
β”œβ”€ sst.json
β”œβ”€ services
β”œβ”€ stacks
└─ my-next-app <-- new Next.js app
β”œβ”€ pages
β”œβ”€ public
β”œβ”€ styles
└─ next.config.js

Continue to step 3.

  1. Alternatively, if you have an existing Next.js app, move the app to the root of your SST app. Your SST app structure should look like:
my-sst-app
β”œβ”€ sst.json
β”œβ”€ services
β”œβ”€ stacks
└─ my-next-app <-- your Next.js app
β”œβ”€ pages
β”œβ”€ public
β”œβ”€ styles
└─ next.config.js
  1. Also add the sst env command to your Next.js app's package.json. sst env enables you to automatically set the environment variables for your Next.js app directly from the outputs in your SST app.
  "scripts": {
- "dev": "next dev",
+ "dev": "sst env \"next dev\"",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
  1. Add the NextjsSite construct to an existing stack in your SST app. You can also create a new stack for the app.
import { NextjsSite, StackContext } as sst from "sst/constructs";

export default function MyStack({ stack }: StackContext) {

// ... existing constructs

// Create the Next.js site
const site = new NextjsSite(stack, "Site", {
path: "my-next-app/",
});

// Add the site's URL to stack output
stack.addOutputs({
URL: site.url,
});
}

When you are building your SST app, NextjsSite will invoke npx open-next@latest build inside the Next.js app directory. Make sure path is pointing to the your Next.js app.

Note that we also added the site's URL to the stack output. After deploy succeeds, the URL will be printed out in the terminal.

Custom domains​

You can configure the website with a custom domain hosted either on Route 53 or externally.

const site = new NextjsSite(stack, "Site", {
path: "my-next-app/",
customDomain: "my-app.com",
});

Note that visitors to the http:// URL will be redirected to the https:// URL.

You can also configure an alias domain to point to the main domain. For example, to setup www.my-app.com redirecting to my-app.com:

const site = new NextjsSite(stack, "Site", {
path: "my-next-app/",
customDomain: {
domainName: "my-app.com",
domainAlias: "www.my-app.com",
},
});

Environment variables​

The NextjsSite construct allows you to set the environment variables in your Next.js app based on outputs from other constructs in your SST app. So you don't have to hard code the config from your backend. Let's look at how.

To expose environment variables to your Next.js application you should utilise the NextjsSite construct environment configuration property rather than an .env file within your Next.js application root.

Imagine you have an S3 bucket created using the Bucket construct, and you want to upload files to the bucket. You'd pass the bucket's name to your Next.js app.

const bucket = new Bucket(stack, "Bucket", {
// ...
});

new NextjsSite(stack, "Site", {
path: "my-next-app/",
environment: {
BUCKET_NAME: bucket.bucketName,
},
});

Then you can access the bucket's name in your server code:

console.log(process.env.BUCKET_NAME);

Note that, in Next.js, only environment variables prefixed with NEXT_PUBLIC_ are available in your browser code. Read more about using environment variables.

For example, if you want to access the bucket's name in your frontend js code, you'd name it NEXT_PUBLIC_BUCKET_NAME:

new NextjsSite(stack, "Site", {
path: "my-next-app/",
environment: {
NEXT_PUBLIC_BUCKET_NAME: bucket.bucketName,
},
});

Let's take look at what is happening behind the scene.

While deploying​

On sst deploy, the Next.js server function is deployed to a Lambda function, and the NextjsSite's environment values are set as Lambda function environment variables. In this case, process.env.BUCKET_NAME will be available at runtime.

If environment variables are referenced in the browser code, they will first be replaced by placeholder values, ie. {{ NEXT_PUBLIC_BUCKET_NAME }}, when building the Next.js app. And after the S3 bucket has been created, the placeholders in the HTML and JS files will then be replaced with the actual values.

caution

Since the actual values are determined at deploy time, you should not rely on the values at build time. For example, you cannot reference process.env.BUCKET_NAME inside getStaticProps() at build time.

There are a couple of work arounds:

  • Hardcode the bucket name
  • Read the bucket name dynamically at build time (ie. from an SSM value)
  • Use fallback pages to generate the page on the fly :::

While developing​

To use these values while developing, run sst dev to start the Live Lambda Development environment.

npx sst dev

Then in your Next.js app to reference these variables, add the sst env command.

package.json
"scripts": {
"dev": "sst env \"next dev\"",
"build": "next build",
"start": "next start"
},

Now you can start your Next.js app as usual and it'll have the environment variables from your SST app.

npm run dev

There are a couple of things happening behind the scenes here:

  1. The sst dev command generates a file with the values specified by the NextjsSite construct's environment prop.
  2. The sst env CLI will traverse up the directories to look for the root of your SST app.
  3. It'll then find the file that's generated in step 1.
  4. It'll load these as environment variables before running the start command.

sst env only works if the Next.js app is located inside the SST app or inside one of its subdirectories. For example:

/
sst.json
my-next-app/

:::

Using AWS services​

Since the NextjsSite construct deploys your Next.js app to your AWS account, it's very convenient to access other resources in your AWS account. NextjsSite provides a simple way to grant permissions to access specific AWS resources.

Imagine you have an S3 bucket created using the Bucket construct, and you want to upload files to the bucket.

const bucket = new Bucket(stack, "Bucket", {
// ...
});

const site = new NextjsSite(stack, "Site", {
path: "my-next-app/",
environment: {
BUCKET_NAME: bucket.bucketName,
},
});

site.attachPermissions([bucket]);

Note that we are also passing the bucket name into the environment, so the Next.js server code can fetch the value process.env.BUCKET_NAME when calling the AWS S3 SDK API to upload a file.

Examples​

Configurng custom domains​

You can configure the website with a custom domain hosted either on Route 53 or externally.

Using the basic config (Route 53 domains)​

new NextjsSite(stack, "Site", {
path: "my-next-app/",
customDomain: "my-app.com",
});

Redirect www to non-www (Route 53 domains)​

new NextjsSite(stack, "Site", {
path: "my-next-app/",
customDomain: {
domainName: "my-app.com",
domainAlias: "www.my-app.com",
},
});

Configuring domains across stages (Route 53 domains)​

new NextjsSite(stack, "Site", {
path: "my-next-app/",
customDomain: {
domainName:
scope.stage === "prod" ? "my-app.com" : `${scope.stage}.my-app.com`,
domainAlias: scope.stage === "prod" ? "www.my-app.com" : undefined,
},
});

Configuring alternate domain names (Route 53 domains)​

You can specify additional domain names for the site url. Note that the certificate for these names will not be automatically generated, so the certificate option must be specified. Also note that you need to manually create the Route 53 records for the alternate domain names.

import * as acm from "aws-cdk-lib/aws-certificatemanager";
import * as route53 from "aws-cdk-lib/aws-route53";
import * as route53Targets from "aws-cdk-lib/aws-route53-targets";

// Look up hosted zone
const hostedZone = route53.HostedZone.fromLookup(stack, "HostedZone", {
domainName: "my-app.com",
});

// Create a certificate with alternate domain names
const certificate = new acm.DnsValidatedCertificate(stack, "Certificate", {
domainName: "foo.my-app.com",
hostedZone,
region: "us-east-1",
subjectAlternativeNames: ["bar.my-app.com"],
});

// Create site
const site = new NextjsSite(stack, "Site", {
path: "my-next-app/",
customDomain: {
domainName: "foo.my-app.com",
alternateNames: ["bar.my-app.com"],
cdk: {
hostedZone,
certificate,
},
},
});

// Create A and AAAA records for the alternate domain names
const recordProps = {
recordName: "bar.my-app.com",
zone: hostedZone,
target: route53.RecordTarget.fromAlias(
new route53Targets.CloudFrontTarget(site.cdk.distribution)
),
};
new route53.ARecord(stack, "AlternateARecord", recordProps);
new route53.AaaaRecord(stack, "AlternateAAAARecord", recordProps);

Importing an existing certificate (Route 53 domains)​

import { Certificate } from "aws-cdk-lib/aws-certificatemanager";

new NextjsSite(stack, "Site", {
path: "my-next-app/",
customDomain: {
domainName: "my-app.com",
cdk: {
certificate: Certificate.fromCertificateArn(stack, "MyCert", certArn),
},
},
});

Note that, the certificate needs be created in the us-east-1(N. Virginia) region as required by AWS CloudFront.

Specifying a hosted zone (Route 53 domains)​

If you have multiple hosted zones for a given domain, you can choose the one you want to use to configure the domain.

import { HostedZone } from "aws-cdk-lib/aws-route53";

new NextjsSite(stack, "Site", {
path: "my-next-app/",
customDomain: {
domainName: "my-app.com",
cdk: {
hostedZone: HostedZone.fromHostedZoneAttributes(stack, "MyZone", {
hostedZoneId,
zoneName,
}),
},
},
});

Configuring externally hosted domain​

import { Certificate } from "aws-cdk-lib/aws-certificatemanager";

new NextjsSite(stack, "Site", {
path: "my-next-app/",
customDomain: {
isExternalDomain: true,
domainName: "my-app.com",
cdk: {
certificate: Certificate.fromCertificateArn(stack, "MyCert", certArn),
},
},
});

Note that the certificate needs be created in the us-east-1(N. Virginia) region as required by AWS CloudFront, and validated. After the Distribution has been created, create a CNAME DNS record for your domain name with the Distribution's URL as the value. Here are more details on configuring SSL Certificate on externally hosted domains.

Also note that you can also migrate externally hosted domains to Route 53 by following this guide.

Configuring server function​

new NextjsSite(stack, "Site", {
path: "my-next-app/",
timeout: "5 seconds",
memorySize: "2048 MB",
});

Configuring image optimization function​

new NextjsSite(stack, "Site", {
path: "my-next-app/",
imageOptimization: {
memorySize: "2048 MB",
}
});

Advanced examples​

Using an existing S3 Bucket​

import * as s3 from "aws-cdk-lib/aws-s3";

new NextjsSite(stack, "Site", {
path: "my-next-app/",
cdk: {
bucket: s3.Bucket.fromBucketName(stack, "Bucket", "my-bucket"),
},
});

Reusing CloudFront cache policies​

CloudFront has a limit of 20 cache policies per AWS account. This is a hard limit, and cannot be increased. If you plan to deploy multiple Next.js sites, you can have the constructs share the same cache policies by reusing them across sites.

import * as cdk from "aws-cdk-lib";
import * as cf from "aws-cdk-lib/aws-cloudfront";

const serverCachePolicy = new cf.CachePolicy(stack, "ServerCache", {
queryStringBehavior: cf.CacheQueryStringBehavior.all(),
headerBehavior: cf.CacheHeaderBehavior.none(),
cookieBehavior: cf.CacheCookieBehavior.all(),
defaultTtl: cdk.Duration.days(0),
maxTtl: cdk.Duration.days(365),
minTtl: cdk.Duration.days(0),
enableAcceptEncodingBrotli: true,
enableAcceptEncodingGzip: true,
});

new NextjsSite(stack, "Site1", {
path: "my-next-app/",
cdk: {
serverCachePolicy,
},
});

new NextjsSite(stack, "Site2", {
path: "another-next-app/",
cdk: {
serverCachePolicy,
},
});

Constructor​

new NextjsSite(scope, id, props)

Parameters

NextjsSiteProps​

bind?​

Type : Array<SSTConstruct>

Bind resources for the function

new NextjsSite(stack, "Function", {
handler: "src/function.handler",
bind: [STRIPE_KEY, bucket],
})

buildCommand?​

Type : string

The command for building the website

buildCommand: "yarn build",

customDomain?​

Type : string | SsrDomainProps

The customDomain for this website. SST supports domains that are hosted either on Route 53 or externally.

Note that you can also migrate externally hosted domains to Route 53 by following this guide.

customDomain: "domain.com",
customDomain: {
domainName: "domain.com",
domainAlias: "www.domain.com",
hostedZone: "domain.com"
},

dev.deploy?​

Type : boolean

When running sst dev, site is not deployed. This is to ensure sst dev` can start up quickly.

dev: {
deploy: true
}

environment?​

Type : Record<string, string>

An object with the key being the environment variable name.

environment: {
API_URL: api.url,
USER_POOL_CLIENT: auth.cognitoUserPoolClient.userPoolClientId,
},

imageOptimization.memorySize?​

Type : number | ${number} MB | ${number} GB

The amount of memory in MB allocated for image optimization function.

memorySize: "512 MB",

memorySize?​

Type : number | ${number} MB | ${number} GB

The amount of memory in MB allocated for SSR function.

memorySize: "512 MB",

path?​

Type : string

Path to the directory where the app is located.

permissions?​

Type : Permissions

Attaches the given list of permissions to the SSR function. Configuring this property is equivalent to calling attachPermissions() after the site is created.

permissions: ["ses"]

runtime?​

Type : "nodejs14.x" | "nodejs16.x" | "nodejs18.x"

The runtime environment for the SSR function.

runtime: "nodejs16.x",

timeout?​

Type : number | ${number} second | ${number} seconds | ${number} minute | ${number} minutes | ${number} hour | ${number} hours | ${number} day | ${number} days

The execution timeout in seconds for SSR function.

timeout: "5 seconds",

waitForInvalidation?​

Type : boolean

While deploying, SST waits for the CloudFront cache invalidation process to finish. This ensures that the new content will be served once the deploy command finishes. However, this process can sometimes take more than 5 mins. For non-prod environments it might make sense to pass in false . That'll skip waiting for the cache to invalidate and speed up the deploy process.

cdk.bucket?​

Type : IBucket | BucketProps

Allows you to override default settings this construct uses internally to ceate the bucket

cdk.distribution?​

Type : SsrCdkDistributionProps

Pass in a value to override the default settings this construct uses to create the CDK Distribution internally.

cdk.id?​

Type : string

Allows you to override default id for this construct.

cdk.server?​

Type : Pick<FunctionProps, "architecture" | "vpc" | "vpcSubnets" | "securityGroups" | "allowAllOutbound" | "allowPublicSubnet">

cdk.serverCachePolicy?​

Type : ICachePolicy

Override the CloudFront cache policy properties for responses from the server rendering Lambda. The default cache policy that is used in the abscene of this property is one that performs no caching of the server response.

Properties​

An instance of NextjsSite has the following properties.

customDomainUrl​

Type : undefined | string

If the custom domain is enabled, this is the URL of the website with the custom domain.

id​

Type : string

url​

Type : undefined | string

The CloudFront URL of the website.

cdk.bucket​

Type : Bucket

cdk.certificate​

Type : undefined | ICertificate

cdk.distribution​

Type : Distribution

cdk.function​

Type : undefined | Function

cdk.hostedZone​

Type : undefined | IHostedZone

The internally created CDK resources.

Methods​

An instance of NextjsSite has the following methods.

attachPermissions​

attachPermissions(permissions)

Parameters

Attaches the given list of permissions to allow the Astro server side rendering to access other AWS resources.

site.attachPermissions(["sns"]);