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β
- 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
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.
- 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
- Also add the
sst env
command to your Next.js app'spackage.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"
},
- 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.
"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:
- The
sst dev
command generates a file with the values specified by theNextjsSite
construct'senvironment
prop. - The
sst env
CLI will traverse up the directories to look for the root of your SST app. - It'll then find the file that's generated in step 1.
- 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
- scope Construct
- id string
- props NextjsSiteProps
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
- permissions Permissions
Attaches the given list of permissions to allow the Astro server side rendering to access other AWS resources.
site.attachPermissions(["sns"]);