Configure a Router
Create a shared CloudFront distribution for your entire app.
You can set custom domains on components like your frontends, APIs, or services. Each of these create their own CloudFront distribution. But as your app grows you might:
- Have multiple frontends, like a landing page, or a docs site, etc.
- Want to serve resources from different paths of the same domain; like
/docs
, or/api
. - Want to set up preview environments on subdomains.
Also since CloudFront distributions can take 15-20 minutes to deploy, creating new distributions for each of the components, and for each stage, can really impact how long it takes to deploy your app.
The ideal setup here is to create a single CloudFront distribution for your entire app and share that across components and across stages.
Let’s look at how to do this with the Router
component.
A sample app
To demo this, let’s say you have the following components in your app.
// Frontendconst web = new sst.aws.Nextjs("MyWeb", { path: "packages/web"});
// APIconst api = new sst.aws.Function("MyApi", { url: true, handler: "packages/functions/api.handler"});
// Docsconst docs = new sst.aws.Astro("MyDocs", { path: "packages/docs"});
This has a frontend, a docs site, and an API. In production we’d like to have:
example.com
serveMyWeb
example.com/api
serveMyApi
docs.example.com
serveMyDocs
In our dev stage we’d like to have:
dev.example.com
serveMyWeb
dev.example.com/api
serveMyApi
docs.dev.example.com
serveMyDocs
For our PR stages or preview environments we’d like to have:
pr-123.dev.example.com
serveMyWeb
pr-123.dev.example.com/api
serveMyApi
docs-pr-123.dev.example.com
serveMyDocs
We are doing docs-pr-123.dev.
instead of docs.pr-123.dev.
because of a limitation with custom domains in CloudFront that we’ll look at below.
Let’s set this up.
Add a router
Instead of adding custom domains to each component, let’s add a Router
to our app with the domain we are going to use in production.
const router = new sst.aws.Router("MyRouter", { domain: { name: "example.com", aliases: ["*.example.com"] }});
The *.example.com
alias is because we want to route to the docs.
subdomain.
And use that in our components.
// Frontendconst web = new sst.aws.Nextjs("MyWeb", { path: "packages/web", router: { instance: router }});
// APIconst api = new sst.aws.Function("MyApi", { url: true, handler: "packages/functions/api.handler", router: { instance: router, path: "/api" }});
// Docsconst docs = new sst.aws.Astro("MyDocs", { path: "packages/docs", router: { instance: router, domain: "docs.example.com" }});
Next, let’s configure the dev stage.
Stage based domains
Since we also want to configure domains for our dev stage, let’s add a function that returns the domain we want, based on the stage.
const domain = $app.stage === "production" ? "example.com" : $app.stage === "dev" ? "dev.example.com" : undefined;
Now when we deploy the dev stage, we’ll create a new Router
with our dev domain.
const router = new sst.aws.Router("MyRouter", { domain: { name: "example.com", aliases: ["*.example.com"] name: domain, aliases: [`*.${domain}`] }});
And update the MyDocs
component to use this.
// Docsconst docs = new sst.aws.Astro("MyDocs", { path: "packages/docs", router: { instance: router, domain: "docs.example.com" domain: `docs.${domain}` }});
Preview environments
Currently, we create a new CloudFront distribution for dev and production. But we want to share the same distribution from dev in our PR stages.
Share the router
To do that, let’s modify how we create the Router
.
const router = new sst.aws.Router("MyRouter", {const router = isPermanentStage ? new sst.aws.Router("MyRouter", { domain: { name: domain, aliases: [`*.${domain}`] } }) : sst.aws.Router.get("MyRouter", "A2WQRGCYGTFB7Z");
The A2WQRGCYGTFB7Z
is the ID of the Router distribution created in the dev stage. You can look this up in the SST Console or output it when you deploy your dev stage.
return { router: router.distributionID};
We are also defining isPermanentStage
. This is set to true
if the stage is dev
or production
.
const isPermanentStage = ["production", "dev"].includes($app.stage);
Let’s also update our domain
helper.
const domain = $app.stage === "production" ? "example.com" : $app.stage === "dev" ? "dev.example.com" : undefined; : `${$app.stage}.dev.example.com`;
Since the domain alias for the dev stage is set to *.dev.example.com
, it can match pr-123.dev.example.com
. But not docs.pr-123.dev.example.com
. This is a limitation of CloudFront.
Nested subdomains
So we’ll be using docs-pr-123.dev.example.com
instead.
To do this, let’s add a helper function.
function subdomain(name: string) { if (isPermanentStage) return `${name}.${domain}`; return `${name}-${domain}`;}
This will add the -
for our PR stages. Let’s update our MyDocs
component to use this.
// Docsconst docs = new sst.aws.Astro("MyDocs", { path: "packages/docs", router: { instance: router, domain: `docs.${domain}` domain: subdomain("docs") }});
Wrapping up
And that’s it! We’ve now configured our router to serve our entire app.
Here’s what the final config looks like.
const isPermanentStage = ["production", "dev"].includes($app.stage);
const domain = $app.stage === "production" ? "example.com" : $app.stage === "dev" ? "dev.example.com" : `${$app.stage}.dev.example.com`;
function subdomain(name: string) { if (isPermanentStage) return `${name}.${domain}`; return `${name}-${domain}`;}
const router = isPermanentStage ? new sst.aws.Router("MyRouter", { domain: { name: domain, aliases: [`*.${domain}`] } }) : sst.aws.Router.get("MyRouter", "A2WQRGCYGTFB7Z");
// Frontendconst web = new sst.aws.Nextjs("MyWeb", { path: "packages/web", router: { instance: router }});
// APIconst api = new sst.aws.Function("MyApi", { url: true, handler: "packages/functions/api.handler", router: { instance: router, path: "/api" }});
// Docsconst docs = new sst.aws.Astro("MyDocs", { path: "packages/docs", router: { instance: router, domain: subdomain("docs") }});
Our components are all sharing the same CloudFront distribution. We also have our PR stages sharing the same router as our dev stage.