Page updated Jan 16, 2024

Server-Side Rendering

SSR functionality in Amplify was primarily built for compatibility with Next.js page components, and their getServerSideProps data fetching mechanism. Compatibility with other frameworks or Next.js features cannot be guaranteed.

To work well with server-rendered pages, Amplify JS requires slight modifications from how you would use it in a client-only environment.

Amplify

Enabling SSR

When using the Amplify CLI, the aws-exports.js file gets created and updated automatically for you based upon the resources you have added and configured.

For client-only apps, Amplify.configure(awsExports) is all you need.

To enable SSR support, also provide ssr: true:

1import { Amplify } from 'aws-amplify';
2import awsExports from '../src/aws-exports';
3
4Amplify.configure({ ...awsExports, ssr: true });

By providing ssr: true, Amplify persists credentials on the client in cookies so that subsequent requests to the server have access to them.

Note: Once vercel/next.js#16977 is resolved, you can hoist Amplify.configure into pages/_app.js. Until then, be sure that all pages/* run Amplify.configure({ ...awsExports, ssr: true }).

withSSRContext

Once an application has been configured with ssr: true, client-side credentials are passed to the server via cookies.

The withSSRContext utility creates an instance of Amplify scoped to a single request (req) using those cookie credentials.

For client-side code rendered in the browser, your page should continue using top-level imports as usual:

1import { Amplify, API } from "aws-amplify";
2import awsExports from "../src/aws-exports";
3
4Amplify.configure({ ...awsExports, ssr: true });
5
6export default function HomePage({ posts = [] }) {
7 const [posts, setPosts] = useState(posts);
8
9 useEffect(() => {
10 // Notice how the client correctly uses the top-level `API` import
11 API.graphql({ query: listPosts }).then(({ data }) => setPosts(data.listPosts.items));
12 }, [])
13
14 return ( ... );
15}

When on the server, use withSSRContext({ req?: ServerRequest }):

1import { Amplify, API, withSSRContext } from 'aws-amplify';
2
3export async function getServerSideProps({ req }) {
4 // Notice how the server uses `API` from `withSSRContext`, instead of the top-level `API`.
5 const SSR = withSSRContext({ req });
6 const { data } = await SSR.API.graphql({ query: listPosts });
7
8 return {
9 props: {
10 posts: data.listPosts.items
11 }
12 };
13}

Server-side functions that don't have a req object (e.g. Next.js' getStaticProps & getStaticPaths) should still use withSSRContext(). Please note that it's not possible to perform authenticated requests with Amplify when using these functions.

Use Amplify with Next.js App Router ("app/" directory)

Amplify JavaScript can be used with the Next.js App Router (Next.js v13.4+) by applying the following changes:

1. Run Amplify.configure({ ...awsExports, ssr: true }) in both the client-side and server-side code

To use Amplify with Next.js App Router, you must run Amplify.configure() in both Client and Server Components. The ssr option must be enabled.

Example Next.js App Router Client Component containing your login flow (e.g. app/page.tsx):

1'use client';
2
3import { Amplify } from 'aws-amplify';
4import awsExports from '@/aws-exports';
5
6Amplify.configure({ ...awsExports, ssr: true });
7
8export default function Home() {
9 return {
10 /* Render a login form for your users */
11 };
12}

Example Next.js App Router Server Component (e.g. app/posts/page.tsx):

1import { Amplify } from 'aws-amplify';
2import awsExports from '@/aws-exports';
3
4Amplify.configure({ ...awsExports, ssr: true });
5
6export default async function Page() {
7 // ...
8}

2. Prepare a request object for withSSRContext to perform server-side operations that require authentication

Example Next.js App Router Server Component that queries data from GraphQL (e.g. app/posts/page.tsx):

1import { Amplify, withSSRContext } from 'aws-amplify';
2import { headers } from 'next/headers';
3import { listPosts } from '@/graphql/queries';
4import awsExports from '@/aws-exports';
5
6Amplify.configure({ ...awsExports, ssr: true });
7
8export default async function Page() {
9 // Construct a req object & prepare an SSR enabled version of Amplify
10 const req = {
11 headers: {
12 cookie: headers().get('cookie')
13 }
14 };
15 const SSR = withSSRContext({ req });
16
17 const { data } = await SSR.API.graphql({ query: listPosts });
18
19 // ...
20}

DataStore

Serializing

For Next.js, returned props from the server have to be valid JSON. Because DataStore.query(Model) returns instances of Model, you need the serializeModel helper to convert it to JSON instead:

1import { serializeModel } from '@aws-amplify/datastore/ssr';
2import { Amplify, withSSRContext } from "aws-amplify";
3
4...
5
6export async function getServerSideProps({ req }) {
7 const SSR = withSSRContext({ req });
8 const posts = await SSR.DataStore.query(Post);
9
10 return {
11 props: {
12 // This converts Post instances into serialized JSON for the client
13 posts: serializeModel(posts),
14 },
15 };
16}

Deserializing

If your client-side code only reads from the server-side props and doesn't perform any updates to these models, then your client-side code won't need any changes.

However, if you receive models from the server and need to DataStore.delete(model) or DataStore.save(...) changes to them, you'll need the deserializeModel utility to convert them from server-friendly JSON back into model instances:

1import { deserializeModel } from '@aws-amplify/datastore/ssr';
2import { Amplify, withSSRContext } from "aws-amplify";
3
4import { Post } from "../src/models";
5
6export default function HomePage(initialData) {
7 // This converts the serialized JSON back into Post instances
8 const [posts, setPosts] = useState(deserializeModel(Post, initialData.posts));
9
10 ...
11}

Example:

Using the following schema:

1type Post
2 @model
3 @auth(rules: [{ allow: owner }, { allow: public, operations: [read] }]) {
4 id: ID!
5 title: String!
6 content: String!
7}

Open pages/index.js and replace it with the following code:

1// pages/index.js
2import Head from 'next/head';
3import styles from '../styles/Home.module.css';
4import {
5 Amplify,
6 Auth,
7 AuthModeStrategyType,
8 DataStore,
9 withSSRContext
10} from 'aws-amplify';
11import { Authenticator } from '@aws-amplify/ui-react';
12import { Post } from '../src/models';
13import { serializeModel } from '@aws-amplify/datastore/ssr';
14import awsExports from '../src/aws-exports';
15
16Amplify.configure({
17 ...awsExports,
18 DataStore: {
19 authModeStrategyType: AuthModeStrategyType.MULTI_AUTH
20 },
21 ssr: true
22});
23
24export async function getServerSideProps({ req }) {
25 const SSR = withSSRContext({ req });
26 const posts = await SSR.DataStore.query(Post);
27
28 return {
29 props: {
30 // This converts Post instances into serialized JSON for the client
31 posts: serializeModel(posts)
32 }
33 };
34}
35
36async function handleCreatePost(event) {
37 event.preventDefault();
38
39 const form = new FormData(event.target);
40
41 try {
42 const post = await DataStore.save(
43 new Post({
44 title: form.get('title'),
45 content: form.get('content')
46 })
47 );
48
49 window.location.href = `/posts/${post.id}`;
50 } catch (error) {
51 console.log(error);
52 }
53}
54
55export default function Home({ posts = [] }) {
56 return (
57 <div className={styles.container}>
58 <Head>
59 <title>Amplify DataStore + Next.js</title>
60 <link rel="icon" href="/favicon.ico" />
61 </Head>
62
63 <main className={styles.main}>
64 <h1 className={styles.title}>Amplify DataStore + Next.js</h1>
65 <p className={styles.description}>
66 <code className={styles.code}>{posts.length}</code>
67 posts
68 </p>
69
70 <div className={styles.grid}>
71 {posts.map((post) => (
72 <a className={styles.card} href={`/posts/${post.id}`} key={post.id}>
73 <h3>{post.title}</h3>
74 <p>{post.content}</p>
75 </a>
76 ))}
77
78 <div className={styles.card}>
79 <h3 className={styles.title}>New Post</h3>
80
81 <Authenticator>
82 <form onSubmit={handleCreatePost}>
83 <fieldset>
84 <legend>Title</legend>
85 <input
86 defaultValue={`Today, ${new Date().toLocaleTimeString()}`}
87 name="title"
88 />
89 </fieldset>
90
91 <fieldset>
92 <legend>Content</legend>
93 <textarea
94 defaultValue="I built an Amplify project with Next.js and DataStore!"
95 name="content"
96 />
97 </fieldset>
98
99 <button>Create Post</button>
100 <button type="button" onClick={() => Auth.signOut()}>
101 Sign out
102 </button>
103 </form>
104 </Authenticator>
105 </div>
106 </div>
107 </main>
108 </div>
109 );
110}