Page updated Nov 15, 2023

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:

import { Amplify } from 'aws-amplify'; import awsExports from '../src/aws-exports'; Amplify.configure({ ...awsExports, 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:

import { Amplify, API } from "aws-amplify"; import awsExports from "../src/aws-exports"; Amplify.configure({ ...awsExports, ssr: true }); export default function HomePage({ posts = [] }) { const [posts, setPosts] = useState(posts); useEffect(() => { // Notice how the client correctly uses the top-level `API` import API.graphql({ query: listPosts }).then(({ data }) => setPosts(data.listPosts.items)); }, []) return ( ... ); }
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 }):

import { Amplify, API, withSSRContext } from 'aws-amplify'; export async function getServerSideProps({ req }) { // Notice how the server uses `API` from `withSSRContext`, instead of the top-level `API`. const SSR = withSSRContext({ req }); const { data } = await SSR.API.graphql({ query: listPosts }); return { props: { posts: data.listPosts.items } }; }
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):

'use client'; import { Amplify } from 'aws-amplify'; import awsExports from '@/aws-exports'; Amplify.configure({ ...awsExports, ssr: true }); export default function Home() { return { /* Render a login form for your users */ }; }
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):

import { Amplify } from 'aws-amplify'; import awsExports from '@/aws-exports'; Amplify.configure({ ...awsExports, ssr: true }); export default async function Page() { // ... }
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):

import { Amplify, withSSRContext } from 'aws-amplify'; import { headers } from 'next/headers'; import { listPosts } from '@/graphql/queries'; import awsExports from '@/aws-exports'; Amplify.configure({ ...awsExports, ssr: true }); export default async function Page() { // Construct a req object & prepare an SSR enabled version of Amplify const req = { headers: { cookie: headers().get('cookie') } }; const SSR = withSSRContext({ req }); const { data } = await SSR.API.graphql({ query: listPosts }); // ... }
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:

import { serializeModel } from '@aws-amplify/datastore/ssr'; import { Amplify, withSSRContext } from "aws-amplify"; ... export async function getServerSideProps({ req }) { const SSR = withSSRContext({ req }); const posts = await SSR.DataStore.query(Post); return { props: { // This converts Post instances into serialized JSON for the client posts: serializeModel(posts), }, }; }
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:

import { deserializeModel } from '@aws-amplify/datastore/ssr'; import { Amplify, withSSRContext } from "aws-amplify"; import { Post } from "../src/models"; export default function HomePage(initialData) { // This converts the serialized JSON back into Post instances const [posts, setPosts] = useState(deserializeModel(Post, initialData.posts)); ... }
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:

type Post @model @auth(rules: [{ allow: owner }, { allow: public, operations: [read] }]) { id: ID! title: String! content: String! }
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:

// pages/index.js import Head from 'next/head'; import styles from '../styles/Home.module.css'; import { Amplify, Auth, AuthModeStrategyType, DataStore, withSSRContext } from 'aws-amplify'; import { Authenticator } from '@aws-amplify/ui-react'; import { Post } from '../src/models'; import { serializeModel } from '@aws-amplify/datastore/ssr'; import awsExports from '../src/aws-exports'; Amplify.configure({ ...awsExports, DataStore: { authModeStrategyType: AuthModeStrategyType.MULTI_AUTH }, ssr: true }); export async function getServerSideProps({ req }) { const SSR = withSSRContext({ req }); const posts = await SSR.DataStore.query(Post); return { props: { // This converts Post instances into serialized JSON for the client posts: serializeModel(posts) } }; } async function handleCreatePost(event) { event.preventDefault(); const form = new FormData(event.target); try { const post = await DataStore.save( new Post({ title: form.get('title'), content: form.get('content') }) ); window.location.href = `/posts/${post.id}`; } catch (error) { console.log(error); } } export default function Home({ posts = [] }) { return ( <div className={styles.container}> <Head> <title>Amplify DataStore + Next.js</title> <link rel="icon" href="/favicon.ico" /> </Head> <main className={styles.main}> <h1 className={styles.title}>Amplify DataStore + Next.js</h1> <p className={styles.description}> <code className={styles.code}>{posts.length}</code> posts </p> <div className={styles.grid}> {posts.map((post) => ( <a className={styles.card} href={`/posts/${post.id}`} key={post.id}> <h3>{post.title}</h3> <p>{post.content}</p> </a> ))} <div className={styles.card}> <h3 className={styles.title}>New Post</h3> <Authenticator> <form onSubmit={handleCreatePost}> <fieldset> <legend>Title</legend> <input defaultValue={`Today, ${new Date().toLocaleTimeString()}`} name="title" /> </fieldset> <fieldset> <legend>Content</legend> <textarea defaultValue="I built an Amplify project with Next.js and DataStore!" name="content" /> </fieldset> <button>Create Post</button> <button type="button" onClick={() => Auth.signOut()}> Sign out </button> </form> </Authenticator> </div> </div> </main> </div> ); }
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}