How to host a Next.js web app with server-side rendering (SSR) in AWS Amplify

🌐   written in English

~‏‏‎ ‎‏‏‎ ‎8m 58s ‎‏‏‎ ‎‏‏‎⌛

Demo links are currently dead

No more waiting! It’s finally here, AWS Amplify hosting for Next.js server-side rendering (SSR)

Why this is great: It’s no joke, I wait for this feature for more than a
year
. And is finally here and is fast!

Not so great: Next 10.x.x was a huge leap forward. All things I wanted to
try, right off the bat, and backed in my applications…But this will be on
hold, as the time of writing, this launch only his support is for Next.js 9.x.x
version. Considering version 10.x is from October 2020 I think the pace is a
little slow to catchup.

To evaluate what type of rendering your application need, I recommend the post
Next.js: Server-side Rendering vs. Static Generation
by Lee Robinson—and in the real world sometimes
you’ll need both.

# The webapp

For all purposes, you could do with your app or create a boilerplate new
NextJS with SSR.
I created this barebones site that renders some info from the
PokéAPI.
Check it out the repository on GitHub.

You can leave all build options as it is, because Amplify will automatically
pick up as SSR and deploy it. For this to happen you also don’t need (or have
to) choose a different export folder. If you clone my repo, you could check that
I’m indeed using NextJS 10.0.0 but, I cannot use any additional feature like
the new Image component.

# Deploying

# If you never used Amplify

You’ll need to have the amplify
installed and configured. Just
follow the docs and you’ll be ready to go. Or make sure you have the latest
version.

# Amplify Init

❯ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project pokessr
The following configuration will be applied:
Project information
| Name: pokessr
| Environment: dev
| Default editor: Visual Studio Code
| App type: javascript
| Javascript framework: react
| Source Directory Path: src
| Distribution Directory Path: build
| Build Command: npm run-script build
| Start Command: npm run-script start

? Initialize the project with the above configuration? Yes
Using default provider awscloudformation
? Select the authentication method you want to use: AWS profile
For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html

? Please choose the profile you want to use amplify

Besides the name, pokessr, I only choose my profile to deploy, called
amplify but you can deploy in whatever profile you configured. I accepted all
defaults. Then amplify will create your environment:

Adding backend environment dev to AWS Amplify Console app: d31r520fbr96mj

⠙ Initializing project in the cloud...

CREATE_IN_PROGRESS amplify-pokessr-dev-185133 AWS::CloudFormation::Stack Tue May 18 2021 18:51:41 GMT-0300 (Horário Padrão de Brasília) User Initiated
CREATE_IN_PROGRESS UnauthRole AWS::IAM::Role Tue May 18 2021 18:51:45 GMT-0300 (Horário Padrão de Brasília)
CREATE_IN_PROGRESS AuthRole AWS::IAM::Role Tue May 18 2021 18:51:45 GMT-0300 (Horário Padrão de Brasília)
CREATE_IN_PROGRESS DeploymentBucket AWS::S3::Bucket Tue May 18 2021 18:51:46 GMT-0300 (Horário Padrão de Brasília)
CREATE_IN_PROGRESS UnauthRole AWS::IAM::Role Tue May 18 2021 18:51:46 GMT-0300 (Horário Padrão de Brasília) Resource creation Initiated
CREATE_IN_PROGRESS AuthRole AWS::IAM::Role Tue May 18 2021 18:51:46 GMT-0300 (Horário Padrão de Brasília) Resource creation Initiated

⠇ Initializing project in the cloud...

CREATE_IN_PROGRESS DeploymentBucket AWS::S3::Bucket Tue May 18 2021 18:51:46 GMT-0300 (Horário Padrão de Brasília) Resource creation Initiated

⠸ Initializing project in the cloud...

CREATE_COMPLETE AuthRole AWS::IAM::Role Tue May 18 2021 18:51:59 GMT-0300 (Horário Padrão de Brasília)
CREATE_COMPLETE UnauthRole AWS::IAM::Role Tue May 18 2021 18:51:59 GMT-0300 (Horário Padrão de Brasília)

⠹ Initializing project in the cloud...

CREATE_COMPLETE DeploymentBucket AWS::S3::Bucket Tue May 18 2021 18:52:08 GMT-0300 (Horário Padrão de Brasília)
CREATE_COMPLETE amplify-pokessr-dev-185133 AWS::CloudFormation::Stack Tue May 18 2021 18:52:10 GMT-0300 (Horário Padrão de Brasília)

✔ Successfully created initial AWS cloud resources for deployments.
✔ Initialized provider successfully.

Initialized your environment successfully.

Your project has been successfully initialized and connected to the cloud!

Some next steps:

“amplify status” will show you what you’ve added already and if it’s locally configured or deployed
“amplify add <category>“ will allow you to add features like user login or a backend API
“amplify push” will build all your local backend resources and provision it in the cloud
“amplify console” to open the Amplify Console and view your project status
“amplify publish” will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

Pro tip:
Try “amplify add api” to create a backend API and then “amplify publish” to deploy everything

What we are going to use is hosting.

And by zero configuration, you just need to connect your repository and the
building settings will be set.

Build settings

And you can always have a look at how the build is going accessing the logs in
the AWS Amplify console. For our purposes, see a Starting SSR Build in your
logs:

2021-05-18T22:35:49.379Z [INFO]: info  - Creating an optimized production build...
2021-05-18T22:35:58.592Z [INFO]: info - Compiled successfully
info - Collecting page data...
2021-05-18T22:35:59.098Z [INFO]: info - Generating static pages (0/28)
2021-05-18T22:35:59.480Z [INFO]: info - Generating static pages (7/28)
2021-05-18T22:35:59.600Z [INFO]: info - Generating static pages (14/28)
2021-05-18T22:35:59.706Z [INFO]: info - Generating static pages (21/28)
2021-05-18T22:35:59.797Z [INFO]: info - Generating static pages (28/28)
2021-05-18T22:35:59.797Z [INFO]: info - Finalizing page optimization...
2021-05-18T22:35:59.814Z [INFO]:
2021-05-18T22:35:59.860Z [INFO]: Page Size First Load JS
┌ λ / 1.32 kB 68.7 kB
├ /_app 0 B 64.2 kB
├ λ /[ditto] 1.39 kB 68.7 kB
├ ○ /404 2.76 kB 66.9 kB
├ ● /pokemons/[name] 1.53 kB 68.9 kB
├ ├ /pokemons/bulbasaur

├ ├ /pokemons/ivysaur

├ ├ /pokemons/venusaur

├ └ [+22 more paths]

└ λ /random 1.39 kB 68.7 kB
+ First Load JS shared by all 64.2 kB
├ chunks/commons.b2f5db.js 13.5 kB
├ chunks/framework.149f13.js 42 kB
├ chunks/main.e0d560.js 6.8 kB
├ chunks/pages/_app.9245f7.js 865 B
├ chunks/webpack.f82c36.js 950 B
└ css/b8e1ed54af27c57535f7.css 897 B

2021-05-18T22:35:59.861Z [INFO]: λ (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)
(ISR) incremental static regeneration (uses revalidate in getStaticProps)

2021-05-18T22:35:59.993Z [INFO]: Starting SSR Build...
2021-05-18T22:37:10.138Z [INFO]: SSR Build Complete.
2021-05-18T22:37:11.159Z [INFO]: # Completed phase: build
2021-05-18T22:37:11.159Z [INFO]: ## Build completed successfully

Then you gave to wait a couple of minutes and your application you be on your
custom domain or in the generate domain of Amplify. For this demonstration my
web apps is available here.

# Pages

The front page is itself server side generated:

export const getServerSideProps: GetServerSideProps = async () => {
const data = await getPokemons();

return {
props: {
data,
},
};
};

It will query in the PokéAPI and return all Pokémons until reaching the number
or the maximum today. I placed 3000 but as you can check, the actual number
today is 1118.

When you click in a Pokémon, I use a dynamic route in the file ditto to
generate the Pokémon by the name. Pokémon fans will get the reference. And
[ditto].tsx is also SSR.

export const getServerSideProps: GetServerSideProps = async (context) => {

let data;
const { ditto} = context.query

if (typeof ditto ===string) {
data = await getPokemonData(ditto)
} else {
data = {}
}

return { props: { data } }

}

But for fun I created a random page… that renders a random Pokémons to test
even better the SSR. It get all the possible Pokémons and returns one at random
using Math.random():


export const getServerSideProps: GetServerSideProps = async () => {

const random = await getPokemons() as Pokedex
const ditto = random.results[random.results.length * Math.random() | 0].name

let data;
if (typeof ditto ===string) {
data = await getPokemonData(ditto)
} else {
data = {}
}

return {
props: {
data
}
}
}

And to test ISG (Incremental Static Generation) I created a folder called
pokemons.
Stable static generation was added to Next 9.3
but my test doesn’t show that works right now with the Amplify SSR hosting, it
defaults to the SSR. ISG is a mechanism to update existing pages, by
re-rendering them in the background as traffic comes in using the property
revalidate. Also, another great use is, per example, you have a specific
dataset of pages to generate at build time but you’ll need on dynamic routes to
be generated new pages as soon you publish another in your headless CMS or
database.
ISG generated even a bounty as feature in another project
and unlocks a lot of interesting use cases.

export const getStaticProps: GetStaticProps = async context => {
let data;
if (context.params) {
data = await getPokemonData(context.params.name as string);
} else {
data = {};
}

return {
props: {
data,
date: new Date().toISOString(),
},
revalidate: 60 * 5,
};
};

export const getStaticPaths: GetStaticPaths<{ name: string }> = async () => {
const pokemons = (await getPokemons(25)) as Pokedex;

const paths = pokemons.results.map(pokemon => {
return { params: { name: pokemon.name.toString() } };
});

return {
fallback: true,
paths,
};
};

For my example, I generate at build time the first 25 Pokémons. See that my
props are changing, I’m passing a dynamic date. But my first 25 Pokémons,
starting with
Bulbasaur and
going up to
Pikachu. They
have a text Generated at that will not revalidate at all (right now configured
to re-validate at each 5 minutes revalidate: 60 * 5 in which it should change
this date. But if you access any other Pokémon than the first 25, like the
number 26, Raichu
or the 186, Scizor
will be server-side generated at the time and you’ll see the date of any time
you accessing again, so no ISG. This feature was not advertised but since it was
stable at 9.3+ and I did not found work and it happens you are after this,
you’ll need to wait or surprise me show-me what I’m getting wrong here because I
would love to have ISG already 😀


Please leave your thoughts, takes and insights on Twitter! Or problems if you
have one or the solution to ISG!