Front-End Web & Mobile

Host a Next.js SSR app with real-time data on AWS Amplify

UPDATE: Amplify Hosting now supports Next.js 12 and 13. Please visit our new launch blog to learn more.

Today, AWS Amplify Hosting announced support for continuously deploying server-side rendering (SSR) apps built with Next.js. With this launch, AWS Amplify CI/CD and Hosting allows you to deploy hybrid Next.js apps that support both static and dynamic rendering of web pages. Amplify’ Hosting is a fully managed build and hosting service that allows developers to easily build, deploy, and host full stack applications on AWS with features such as continuous deployment, PR previews, easy custom domains, and password protection. Next.js is a JavaScript framework built on React that helps developers build more performant, SEO-friendly apps with features such as pre-rendering, automatic code splitting, dynamic API routes.

In this blog post we are going to deploy a Next.js app on the AWS Amplify console, set up a custom domain, enable auto branch detection, and then set up a backend database for the app.

Setup SSR app

To get started, first let’s bootstrap a Next.js project on your local machine.

npx create-next-app
# or
yarn create next-app
yarn add react-markdown

To start developing your application run npm run dev or yarn dev. This starts the development server on http://localhost:3000. You should see the default Next.js app render on your browser.

 

Welcome to Next.js browser screen on localhost

 

Before we start modifying the app, let’s walkthrough the structure:

  • /pages: The pages folder contains all your app’s web pages. In Next.js, a page is a React component, and each page is associated with a route based on its filename. For example, pages/about.js will be accessible at localhost:3000/about.
  • /api: Another key feature that Next.js offers is API routes. Any file inside the folder pages/api is mapped to /api/* and will be treated as an API endpoint instead of a page. The default app comes with pages/api/hello.js that provides an API route. You can use this API to interface with any backend service to fetch data.

Zero config CI/CD deployments with AWS Amplify

Deploying your app is easy – simply commit your code to a a Git provider of choice – Amplify supports AWS CodeCommit, GitHub, Bitbucket, and GitLab. Once your code has been checked in, simply connect your repository in the Amplify Console. Amplify automatically detects your build settings and deploys your connected branch in a few minutes. Any server-rendered pages or API routes get deployed as resources in your account.

To get started visit the Amplify Console inside the AWS console. If this is your first time using Amplify, there are two ways to get started – choose ‘Host your web app’ to connect your Git repository. If you already have Amplify projects, select New app > Host web app from the All apps page.

 

AWS Amplify Sandbox create an app backend or host your web app options

 

Once you connect a repository and branch you will be able taken to the build configuration screen. Amplify will automatically detect that your app is an SSR app. There’s no other changes required – Amplify automatically detects your build settings for you so you are good to go. Simply click complete the wizard and your app deployment will begin.

 

AWS Amplify Build and test settings

After a few minutes the Amplify console pipeline will light up green, and your app is now deployed successfully! Go ahead and click on the screenshot to open your deployed app.

 

Amplify app frontend environment fully deployed

Click on the build details to view the deployment details. As you can see, server-rendered pages or API routes get deployed in your account as Lambda functions served via CloudFront. As of November 17th 2022, Amplify manages your server-renderered resources for you, so you will no longer see these additional resources in your account.

 

Now let’s make a code change and commit it to automatically trigger a build. On your local machine, open pages/index.js and replace the content with the following:

import Head from 'next/head'
import styles from '../styles/Home.module.css'

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          My Next.js Amplify app 
        </h1>
      </main>
    </div>
  )
}

Commit your code to Git. You should immediately notice that a new build has been triggered in the Amplify console. After the build succeeds, your app UI should update to show the latest changes.

 

My Next.js Amplify App deployed on Amplify backend

You should also be able to access your API route at amplifyapp.com/api/hello

 

api/hello route of Next.js app showing user object in JSON

Easy custom domains, automatic branch detection, PR previews, and more

The primary benefit deploying Next.js apps on Amplify is that you get all the Amplify Hosting capabilities out of the box. For this blog, we will setup Amplify to watch our repository for any branches, and automatically connect any new branches. We can also set it up to automatically disconnect branches when a branch is deleted from the repository. In the Amplify console, go to App settings > General > Edit. Set a ‘*’ pattern so Amplify watches for any changes to your repo – alternatively you can set patterns such as ‘feature*’ to only pick up branches that match a certain pattern.

 

Amplify Console enable branch autodetection

Custom domains are easy – if you manage your domain in AWS, setting up your domain is as simple as picking your domain from a dropdown and configuring subdomains pointing to branches. Once you hit ‘Save’, Amplify automatically generates an SSL certificate for you and sets up the custom domain for your feature branches.

 

App settings: Domain management - Add domain

Amplify Console set subdomains

 

Add a database with real-time subscriptions to the SSR app

Now for the fun part – let’s deploy an app backend with a database and connect to the database from our app from the Admin UI. Launched at re:Invent 2020, the AWS Amplify Admin UI is an externally hosted console for frontend teams to visually create and manage an app backend. Once the backend is setup, we will use the Amplify JavaScript library to connect to the backend.

To get started navigate to the ‘Backend environment’ tab and hit ‘Get started’. This will set up a default ‘staging’ Amplify environment from where you can access the Admin UI.

 

Amplify App Get Started Backend environments

From the Admin UI, choose ‘Data’ to get started. Admin UI’s data model designer allows you to build a backend by focusing on your domain-specific objects (e.g., product with description and price), relationships (e.g., products have many orders), and authorization rules (e.g., only signed in users can order a product) instead of setting up database tables, APIs, and authentication infrastructure. For this blog, we’ll build a Posts model as shown below. Once your model looks like the image below, hit Save and deploy.

 

Admin UI Data modeling of Post object

Now that your database is deployed, let’s connect to it from your app. From your local machine, install the Amplify CLI and run the amplify pull --appid XXX envName XXX command (you can find this from the Local setup instructions in the Admin UI header). Accept all the defaults and continue. Install the Amplify JS client library by running yarn add aws-amplify

Update your _app.js to connect to your Amplify backend – the aws-exports file contains all the relevant information about your backend.

...

import Amplify from 'aws-amplify';
import config from '../src/aws-exports';
Amplify.configure({
  ...config, ssr: true
});

...

 

Update your index.js with the following code. Amplify DataStore is an on-device storage engine that automatically synchronizes data between your mobile and web apps and your database in the AWS cloud to help you build real-time and offline apps faster. In the code below, DataStore.query fetches all the posts, and DataStore.observe watches the database for changes.

import styles from '../styles/Home.module.css'
import { DataStore } from 'aws-amplify'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { Post } from '../src/models'

export default function Home() {
  const [posts, setPosts] = useState([])

  useEffect(() => {
    fetchPosts()
    async function fetchPosts() {
      const postData = await DataStore.query(Post)
      setPosts(postData)
    }
    const subscription = DataStore.observe(Post).subscribe(() => fetchPosts())
    return () => subscription.unsubscribe()
  }, [])
  
  return (
    <div className={styles.container}>
      <h1>Posts</h1>
      {
        posts.map(post => (
          <Link href={`/posts/${post.id}`}>
            <a>
              <h2>{post.title}</h2>
            </a>
          </Link>
        ))
      }
    </div>
  )
}

Let’s populate our database with some data. Head back to the Admin UI and navigate to the Content tab. Enter a single record as shown below. This data should also show up in your app in real-time.

 

Admin UI Content section. Single Post entry

My Next.js fullstack app shown on localhost

Awesome! We have now set up a database and connected to it from our frontend. As a bonus, we can implement dynamic routing using Next.js’s routing, getStaticProps, and getStaticPath capabilities. Given our data is dynamic in nature, we will need to use Next.js’s dynamic routing capabilities in order to view a post detail for a path we will most likely not know at build time. Create a file called pages/[id].js and paste the following code:

import { withSSRContext } from 'aws-amplify'
import { Post } from '../../src/models'
import Markdown from 'react-markdown'
import { useRouter } from 'next/router'

export default function PostComponent({ post }) {
  const router = useRouter()
  if (router.isFallback) {
    return <div>Loading...</div>
  }
  return (
    <div>
      <Markdown children={post.content} />
    </div>
  )
}

export async function getStaticPaths(req) {
  const { DataStore } = withSSRContext(req)
  const posts = await DataStore.query(Post)
  const paths = posts.map(post => ({ params: { id: post.id }}))
  return {
    paths,
    fallback: true,
  }
}

export async function getStaticProps (req) {
  const { DataStore } = withSSRContext(req)
  const { params } = req
  const { id } = params
  const post = await DataStore.query(Post, id)

  return {
    props: {
      post: JSON.parse(JSON.stringify(post))
    },
    revalidate: 1
  }

}

You will now be able to click on a link from localhost:3000 to view the details of post at the unique ID of the post. This post will get statically generated for the next build allowing the content to automatically get pre-rendered.

 

localhost - posts/postId path - This is my new post

Before we push our changes to Amplify, we need to point our dev branch to our staging backend. From the Amplify console, choose ‘Edit’ next to the label that says ‘Continuous deploys setup’ (under the branch name) and point the dev branch to the staging backend. This will ensure the build is able to properly point to the backend. Now simply push your code to git and you should see your changes live.

Resources

To summarize, we hosted SSR feature branches, enabled branch autodetection, custom domains, and then added an app backend. To learn more visit the following resources:

Thanks for reading!