Skip to main content

NextjsSite

The NextjsSite construct is a higher level CDK construct that lets you create Next.js apps on AWS. It uses OpenNext to build your Next.js app, and transforms the build output to a format that can be deployed to AWS.

Here's how it works at a high level.

  • 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. You can deploy to Lambda@Edge instead if the edge flag is enabled. Read more about Single region vs Edge.
  • You can reference other AWS resources directly in your Next.js app.
  • You can configure custom domains.

Quick Start

  1. You can use SST in an existing Next.js app in drop-in mode or inside a monorepo app in standalone mode.

    • If you have an existing Next.js app, just run npx create-sst at the root and it'll configure SST in drop-in mode.

      npx create-sst@latest
    • If you are starting from scratch, we recommend using our monorepo starter in standalone mode.

      npx create-sst@latest --template standard/nextjs
  2. This adds the NextjsSite construct to your stacks code.

    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: "packages/web",
    });

    // 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. We also print out the site.url once deployed.

  3. We also use the sst bind command in your Next.js app's package.json to run next dev. This allows you to bind your AWS resources directly to your Next.js app.

      "scripts": {
    - "dev": "next dev",
    + "dev": "sst bind next dev",
    "build": "next build",
    },

Check out the full Next.js tutorial.


Working locally

To work on your Next.js app locally with SST:

  1. Start SST in your project root.

    npx sst dev
  2. Then start your Next.js app. This should run sst bind next dev.

    npm run dev
note

When running sst dev, SST does not deploy your Next.js app. It's meant to be run locally.


Single region vs edge

There are two ways you can deploy a Next.js app to your AWS account.

  • Single region

    By default, the Next.js app server is deployed to a single region defined in your sst.config.ts or passed in via the --region flag.

  • Edge

    Alternatively, you can choose to deploy to the edge. When deployed to the edge, middleware, SSR functions, and API routes are running on edge location that is physically closer to the end user. In this case, the app server is deployed to AWS Lambda@Edge.

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

Note that, if you have a centralized database, Edge locations are often far away from your database. If you are querying your database in your SSR functions and API routes, you might experience much longer latency when deployed to the edge.

info

If you are not sure which one to use, we recommend deploying to a single region.


Custom domains

You can configure the app 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 http:// will be redirected to https://.

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

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

Using AWS services

SST makes it very easy for your NextjsSite construct to access other resources in your AWS account. Imagine you have an S3 bucket created using the Bucket construct. You can bind it to your Next.js app.

const bucket = new Bucket(stack, "Uploads");

const site = new NextjsSite(stack, "Site", {
path: "packages/web",
bind: [bucket],
});

This will attach the necessary IAM permissions and allow your Next.js app to access the bucket through the typesafe sst/node client.

import { Bucket } from "sst/node/bucket";

export async function getServerSideProps() {
console.log(Bucket.Uploads.bucketName);
}

You can read more about this over on the Resource Binding doc.


Warming

Server functions may experience performance issues due to Lambda cold starts. SST helps mitigate this by periodically invoking the server function.

new NextjsSite(stack, "Site", {
path: "packages/web",
warm: 20,
});

Setting warm to 20 keeps 20 server function instances active, invoking them every 5 minutes.

Note that warming is currently supported only in regional mode.

Read more about how warming works and the associated cost.


Client side environment variables

You can also pass in environment variables directly to your client side code.

const bucket = new Bucket(stack, "Bucket");

new NextjsSite(stack, "Site", {
path: "packages/web",
environment: {
NEXT_PUBLIC_BUCKET_NAME: bucket.bucketName,
},
});

Now you can access the bucket's name in your client side code.

console.log(process.env.NEXT_PUBLIC_BUCKET_NAME);

In Next.js, only environment variables prefixed with NEXT_PUBLIC_ are available in your client side code. Read more about using environment variables over on the Next.js docs.

You can also read about how this works behind the scenes in SST.


Examples

Configuring 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:
stack.stage === "prod" ? "my-app.com" : `${stack.stage}.my-app.com`,
domainAlias: stack.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 { DnsValidatedCertificate } from "aws-cdk-lib/aws-certificatemanager";
import { HostedZone, RecordTarget, ARecord, AaaaRecord } from "aws-cdk-lib/aws-route53";
import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets";

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

// Create a certificate with alternate domain names
const certificate = new 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: RecordTarget.fromAlias(
new CloudFrontTarget(site.cdk.distribution)
),
};
new ARecord(stack, "AlternateARecord", recordProps);
new 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

Configuring VPC

Note that VPC is only supported when deploying to a single region.

import { Vpc, SubnetType } from "aws-cdk-lib/aws-ec2";

// Create a VPC
const vpc = new Vpc(stack, "myVPC");

// Alternatively use an existing VPC
const vpc = Vpc.fromLookup(stack, "myVPC", { ... });

new NextjsSite(stack, "Site", {
path: "my-next-app/",
cdk: {
server: {
vpc,
vpcSubnets: {
subnetType: SubnetType.PRIVATE_WITH_NAT,
}
}
}
});

Configuring log retention

import { RetentionDays } from "aws-cdk-lib/aws-logs";

new NextjsSite(stack, "Site", {
path: "my-next-app/",
cdk: {
server: {
logRetention: RetentionDays.ONE_MONTH,
}
},
});

Using an existing S3 Bucket

import { Bucket } from "aws-cdk-lib/aws-s3";

new NextjsSite(stack, "Site", {
path: "my-next-app/",
cdk: {
bucket: 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 { Duration } from "aws-cdk-lib";
import {
CachePolicy,
CacheQueryStringBehavior,
CacheHeaderBehavior,
CacheCookieBehavior,
} from "aws-cdk-lib/aws-cloudfront";

const serverCachePolicy = new CachePolicy(stack, "ServerCache", {
queryStringBehavior: CacheQueryStringBehavior.all(),
headerBehavior: CacheHeaderBehavior.none(),
cookieBehavior: CacheCookieBehavior.all(),
defaultTtl: Duration.days(0),
maxTtl: Duration.days(365),
minTtl: 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,
},
});

Configuring CloudFront response headers policies

import { ResponseHeadersPolicy } from "aws-cdk-lib/aws-cloudfront";

new NextjsSite(stack, "Site", {
path: "my-next-app/",
cdk: {
responseHeadersPolicy: ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS,
},
});

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
}

dev.url?

Type : string

The local site URL when running sst dev .

dev: {
url: "http://localhost:3000"
}

edge?

Type : boolean

The SSR function is deployed to Lambda in a single region. Alternatively, you can enable this option to deploy to Lambda@Edge.

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.

warm?

Type : number

The number of server functions to keep warm. This option is only supported for the regional mode.

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.responseHeadersPolicy?

Type : IResponseHeadersPolicy

Override the CloudFront response headers policy properties for responses from the server rendering Lambda.

cdk.server?

Type : Pick<FunctionProps, "architecture" | "logRetention" | "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

Type : undefined |

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 server side rendering framework to access other AWS resources.

site.attachPermissions(["sns"]);