Page updated Nov 25, 2023

Quickstart

This Quickstart guide will walk you through how to build a task list application with TypeScript, Next.js, and React. If you are new to these technologies, we recommend you go through the official React, Next.js, and TypeScript tutorials first.

Prerequisites

Before you get started, make sure you have the following installed:

Create a project

First, you will need to create a new Next.js app. The following command will create a Next.js app in a directory called next-amplify-gen2.

npm create next-app@14 -- next-amplify-gen2 --typescript --eslint --no-app --no-src-dir --no-tailwind --import-alias '@/*' cd next-amplify-gen2
1npm create next-app@14 -- next-amplify-gen2 --typescript --eslint --no-app --no-src-dir --no-tailwind --import-alias '@/*'
2cd next-amplify-gen2

The easiest way to get started with AWS Amplify is through npm with create-amplify.

npm create amplify@latest
1npm create amplify@latest
? Where should we create your project? (.) # press enter
1? Where should we create your project? (.) # press enter

Running this command will scaffold a lightweight Amplify project in your current project with the following files:

├── amplify/ │ ├── auth/ │ │ └── resource.ts │ ├── data/ │ │ └── resource.ts │ ├── backend.ts │ └── package.json ├── node_modules/ ├── .gitignore ├── package-lock.json ├── package.json └── tsconfig.json
1├── amplify/
2│ ├── auth/
3│ │ └── resource.ts
4│ ├── data/
5│ │ └── resource.ts
6│ ├── backend.ts
7│ └── package.json
8├── node_modules/
9├── .gitignore
10├── package-lock.json
11├── package.json
12└── tsconfig.json

Start local dev server

Let's start a local dev server for your app development. For the frontend, run npm run dev to spin up a localhost dev server with the default Next.js template.

npm run dev
1npm run dev

Now configure your AWS account to use Amplify. Note: If you already have an AWS profile with credentials on your local machine, and you have configured the corresponding AWS profile with the AmplifyBackendDeployFullAccess permission policy, you can skip this step.

For the backend, we're going to start a cloud sandbox environment. Amplify gives every developer a personal cloud sandbox environment that provides isolated development spaces to rapidly build, test, and iterate. When you're working with a team, each developer will have their own personal cloud sandbox. In a new terminal window, run the following command:

npx amplify sandbox
1npx amplify sandbox

Do not use cloud sandbox environments in production.

You should now have these two commands running concurrently in different terminal windows.

Build a backend

Next, we will add data and auth capabilities to the app.

Add data

Each of the resource.ts files is where you define the corresponding backend resource. If you open up the /amplify/data/resource.ts file in your text editor, you will see some generated code already there:

// amplify/data/resource.ts import { type ClientSchema, a, defineData } from '@aws-amplify/backend'; /*== STEP 1 =============================================================== The section below creates a Todo database table with a "content" field. Try adding a new "isDone" field as a boolean. The authorization rules below specify that owners, authenticated via your Auth resource, can "create", "read", "update", and "delete" their own records. Public users, authenticated via an API key, can only "read" records. =========================================================================*/ const schema = a.schema({ Todo: a .model({ content: a.string() }) .authorization([a.allow.owner(), a.allow.public().to(['read'])]) }); export type Schema = ClientSchema<typeof schema>; export const data = defineData({ schema, authorizationModes: { defaultAuthorizationMode: 'apiKey', // API Key is used for a.allow.public() rules apiKeyAuthorizationMode: { expiresInDays: 30 } } }); /*== STEP 2 =============================================================== Go to your frontend source code. From your client-side code, generate a Data client to make CRUDL requests to your table. (THIS SNIPPET WILL ONLY WORK IN THE FRONTEND CODE FILE.) Using JavaScript or Next.js React Server Components, Middleware, Server Actions, or Pages Router? Review how to generate Data clients for those use cases: https://docs.amplify.aws/gen2/build-a-backend/data/connect-to-API/ =========================================================================*/ /* "use client" import { generateClient } from "aws-amplify/data"; import { type Schema } from "@/amplify/data/resource"; const client = generateClient<Schema>() // use this Data client for CRUDL requests */ /*== STEP 3 =============================================================== Fetch records from the database and use them in your frontend component. (THIS SNIPPET WILL ONLY WORK IN THE FRONTEND CODE FILE.) =========================================================================*/ /* For example, in a React component, you can use this snippet in your function's RETURN statement */ // const { data: todos } = client.models.Todo.list() // return <ul>{todos.map(todo => <li key={todo.id}>{todo.content}</li>)}</ul>
1// amplify/data/resource.ts
2import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
3
4/*== STEP 1 ===============================================================
5The section below creates a Todo database table with a "content" field. Try
6adding a new "isDone" field as a boolean. The authorization rules below
7specify that owners, authenticated via your Auth resource, can "create",
8"read", "update", and "delete" their own records. Public users,
9authenticated via an API key, can only "read" records.
10=========================================================================*/
11const schema = a.schema({
12 Todo: a
13 .model({
14 content: a.string()
15 })
16 .authorization([a.allow.owner(), a.allow.public().to(['read'])])
17});
18
19export type Schema = ClientSchema<typeof schema>;
20
21export const data = defineData({
22 schema,
23 authorizationModes: {
24 defaultAuthorizationMode: 'apiKey',
25 // API Key is used for a.allow.public() rules
26 apiKeyAuthorizationMode: {
27 expiresInDays: 30
28 }
29 }
30});
31
32/*== STEP 2 ===============================================================
33Go to your frontend source code. From your client-side code, generate a
34Data client to make CRUDL requests to your table. (THIS SNIPPET WILL ONLY
35WORK IN THE FRONTEND CODE FILE.)
36
37Using JavaScript or Next.js React Server Components, Middleware, Server
38Actions, or Pages Router? Review how to generate Data clients for those use
39cases: https://docs.amplify.aws/gen2/build-a-backend/data/connect-to-API/
40=========================================================================*/
41
42/*
43"use client"
44import { generateClient } from "aws-amplify/data";
45import { type Schema } from "@/amplify/data/resource";
46
47const client = generateClient<Schema>() // use this Data client for CRUDL requests
48*/
49
50/*== STEP 3 ===============================================================
51Fetch records from the database and use them in your frontend component.
52(THIS SNIPPET WILL ONLY WORK IN THE FRONTEND CODE FILE.)
53=========================================================================*/
54
55/* For example, in a React component, you can use this snippet in your
56 function's RETURN statement */
57// const { data: todos } = client.models.Todo.list()
58
59// return <ul>{todos.map(todo => <li key={todo.id}>{todo.content}</li>)}</ul>
Learn more
What is a schema?

The schema generated by Amplify is for a to-do app. A schema is a blueprint for how our app's data will be organized. Within the schema, we will define models which will correspond to a database table—Todo in the above code. Finally, we will define fields which are attributes that each data instance will have—in the generated code, the fields are name and description. Each field will have a type attached to it—in the above examples, we are stating that both name and description are strings.

Let's make a quick change to our data model and add a done field. This will be a boolean, which can be set to true or false depending on whether our to-do list item is complete. Let's also add a priority field, which will be an enum. This field type will allow for just a few options to be stored—low, medium, or high.

We've removed the default comments to shorten the code below; however, you can choose to keep them for help in connecting to your frontend resources.

// amplify/data/resource.ts import { type ClientSchema, a, defineData } from '@aws-amplify/backend'; // ... const schema = a.schema({ Todo: a .model({ content: a.string(), + done: a.boolean(), + priority: a.enum(['low', 'medium', 'high']) }) .authorization([a.allow.owner(), a.allow.public().to(['read'])]), }); export type Schema = ClientSchema<typeof schema>; export const data = defineData({ schema, authorizationModes: { defaultAuthorizationMode: 'apiKey', // API Key is used for a.allow.public() rules apiKeyAuthorizationMode: { expiresInDays: 30, }, }, }); // ...
1// amplify/data/resource.ts
2import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
3
4// ...
5
6const schema = a.schema({
7 Todo: a
8 .model({
9 content: a.string(),
10+ done: a.boolean(),
11+ priority: a.enum(['low', 'medium', 'high'])
12 })
13 .authorization([a.allow.owner(), a.allow.public().to(['read'])]),
14});
15
16export type Schema = ClientSchema<typeof schema>;
17
18export const data = defineData({
19 schema,
20 authorizationModes: {
21 defaultAuthorizationMode: 'apiKey',
22 // API Key is used for a.allow.public() rules
23 apiKeyAuthorizationMode: {
24 expiresInDays: 30,
25 },
26 },
27});
28
29// ...

Once you save your changes to the data model, your changes will be deployed in seconds within your cloud sandbox.

Our Todo data model also has authorization set up. Our model has the authorization method chained to it, which you can add rules to. In our example, we are allowing an owner, or the person who creates the Todo instance, to perform all actions on the data they own. We are also allowing all page viewers, including unauthenticated users, to read data. These can be modified; for example, we could remove the .to(['read']) and allow all visitors to perform all actions on data. We could also add permissions for signed-in users or users who belong to user groups such as Admin. You can learn more about all options for authorization in the customize your auth rules section of the docs.

Let's remove public access.

.authorization([a.allow.owner()]),
1.authorization([a.allow.owner()]),

Then, you will see the defineData function, which has our schema and authorization configuration passed in as arguments. We have an apiKey set up to enable public access. Let's change our defaultAuthorizationMode to userPool instead so that the default is to use user authentication.

export const data = defineData({ schema, authorizationModes: { defaultAuthorizationMode: 'userPool', }, });
1export const data = defineData({
2 schema,
3 authorizationModes: {
4 defaultAuthorizationMode: 'userPool',
5 },
6});

The definitions are imported and set in the backend file. We will not need to change anything in this file right now, but you can see its contents, which define a backend with our data and auth configurations from their respective files.

// amplify/backend.ts import { defineBackend } from '@aws-amplify/backend'; import { auth } from './auth/resource.js'; import { data } from './data/resource.js'; defineBackend({ auth, data });
1// amplify/backend.ts
2import { defineBackend } from '@aws-amplify/backend';
3import { auth } from './auth/resource.js';
4import { data } from './data/resource.js';
5
6defineBackend({
7 auth,
8 data
9});

Add authentication

Now let's work on our authentication configuration. Similar to the data/resource.ts we just worked on, the auth/resource.ts file has code to define our authentication configuration. In this case, setting the authentication method to log in with email.

// amplify/auth/resource.ts import { defineAuth } from '@aws-amplify/backend'; /** * Define and configure your auth resource * When used alongside data, it is automatically configured as an auth provider for data * @see https://docs.amplify.aws/gen2/build-a-backend/auth */ export const auth = defineAuth({ loginWith: { email: true, // add social providers externalProviders: { /** * first, create your secrets using `amplify sandbox secret` * then, import `secret` from `@aws-amplify/backend` * @see https://docs.amplify.aws/gen2/deploy-and-host/sandbox-environments/features/#setting-secrets */ // loginWithAmazon: { // clientId: secret('LOGINWITHAMAZON_CLIENT_ID'), // clientSecret: secret('LOGINWITHAMAZON_CLIENT_SECRET'), // } } }, /** * enable multifactor authentication * @see https://docs.amplify.aws/gen2/build-a-backend/auth/manage-mfa */ // multifactor: { // mode: 'OPTIONAL', // sms: { // smsMessage: (code) => `Your verification code is ${code}`, // }, // }, userAttributes: { /** request additional attributes for your app's users */ // profilePicture: { // mutable: true, // required: false, // }, } });
1// amplify/auth/resource.ts
2import { defineAuth } from '@aws-amplify/backend';
3
4/**
5 * Define and configure your auth resource
6 * When used alongside data, it is automatically configured as an auth provider for data
7 * @see https://docs.amplify.aws/gen2/build-a-backend/auth
8 */
9export const auth = defineAuth({
10 loginWith: {
11 email: true,
12 // add social providers
13 externalProviders: {
14 /**
15 * first, create your secrets using `amplify sandbox secret`
16 * then, import `secret` from `@aws-amplify/backend`
17 * @see https://docs.amplify.aws/gen2/deploy-and-host/sandbox-environments/features/#setting-secrets
18 */
19 // loginWithAmazon: {
20 // clientId: secret('LOGINWITHAMAZON_CLIENT_ID'),
21 // clientSecret: secret('LOGINWITHAMAZON_CLIENT_SECRET'),
22 // }
23 }
24 },
25 /**
26 * enable multifactor authentication
27 * @see https://docs.amplify.aws/gen2/build-a-backend/auth/manage-mfa
28 */
29 // multifactor: {
30 // mode: 'OPTIONAL',
31 // sms: {
32 // smsMessage: (code) => `Your verification code is ${code}`,
33 // },
34 // },
35 userAttributes: {
36 /** request additional attributes for your app's users */
37 // profilePicture: {
38 // mutable: true,
39 // required: false,
40 // },
41 }
42});

Let's customize the verification email for our app. We can add a subject line by defining an object with email authentication properties. Again, we have removed comments for brevity.

// amplify/auth/resource.ts import { defineAuth } from '@aws-amplify/backend'; export const auth = defineAuth({ loginWith: { - email: true, + email: { + verificationEmailSubject: 'Welcome! Verify your email!' + }, // add social providers externalProviders: { } } });
1// amplify/auth/resource.ts
2import { defineAuth } from '@aws-amplify/backend';
3
4export const auth = defineAuth({
5 loginWith: {
6- email: true,
7+ email: {
8+ verificationEmailSubject: 'Welcome! Verify your email!'
9+ },
10 // add social providers
11 externalProviders: {
12 }
13 }
14});

Build UI

Let's add UI that connects to the backend data and auth resources.

Add a login form

First, install the Amplify UI component library:

npm add @aws-amplify/ui-react
1npm add @aws-amplify/ui-react

Next, in your app's pages/_app.tsx, add the following imports and wrap your App in the withAuthenticator function:

// pages/_app.tsx import { withAuthenticator } from '@aws-amplify/ui-react'; import { Amplify } from 'aws-amplify'; import config from '@/amplifyconfiguration.json'; import '@aws-amplify/ui-react/styles.css'; import type { AppProps } from 'next/app'; // configure the Amplify client library with the configuration generated by `amplify sandbox` Amplify.configure(config); function App({ Component, pageProps }: AppProps) { return <Component {...pageProps} />; } export default withAuthenticator(App);
1// pages/_app.tsx
2import { withAuthenticator } from '@aws-amplify/ui-react';
3import { Amplify } from 'aws-amplify';
4import config from '@/amplifyconfiguration.json';
5import '@aws-amplify/ui-react/styles.css';
6import type { AppProps } from 'next/app';
7
8// configure the Amplify client library with the configuration generated by `amplify sandbox`
9Amplify.configure(config);
10
11function App({ Component, pageProps }: AppProps) {
12 return <Component {...pageProps} />;
13}
14
15export default withAuthenticator(App);

Run your application with npm run dev and navigate to http://localhost:3000. You should now see the authenticator, which is already configured and ready for your first sign-up! Create a new user account, confirm the account through email, and then sign in.

View list of to-do items

Now, let's display data on our app's frontend. Modify your app's home page file, pages/index.tsx, with the following code:

// pages/index.tsx import { useState, useEffect } from 'react'; import { generateClient } from 'aws-amplify/data'; import { Schema } from '@/amplify/data/resource'; // generate your data client using the Schema from your backend const client = generateClient<Schema>(); export default function HomePage() { const [todos, setTodos] = useState<Schema['Todo'][]>([]); async function listTodos() { // fetch all todos const { data } = await client.models.Todo.list(); setTodos(data); } useEffect(() => { listTodos(); }, []); return ( <main> <h1>Hello, Amplify 👋</h1> <ul> {todos.map((todo) => ( <li key={todo.id}>{todo.content}</li> ))} </ul> </main> ); }
1// pages/index.tsx
2import { useState, useEffect } from 'react';
3import { generateClient } from 'aws-amplify/data';
4import { Schema } from '@/amplify/data/resource';
5
6// generate your data client using the Schema from your backend
7const client = generateClient<Schema>();
8
9export default function HomePage() {
10 const [todos, setTodos] = useState<Schema['Todo'][]>([]);
11
12 async function listTodos() {
13 // fetch all todos
14 const { data } = await client.models.Todo.list();
15 setTodos(data);
16 }
17
18 useEffect(() => {
19 listTodos();
20 }, []);
21
22 return (
23 <main>
24 <h1>Hello, Amplify 👋</h1>
25 <ul>
26 {todos.map((todo) => (
27 <li key={todo.id}>{todo.content}</li>
28 ))}
29 </ul>
30 </main>
31 );
32}

Once you save the file and navigate back to http://localhost:3000, you should see an empty list of to-dos.

Create a new to-do item

Let's update the return statement of the above component to also have a button for creating a new to-do list item, prompting the user to add the title and description. In a production app, you would want to create a full form, and run the create method on form submission.

// pages/index.tsx // ... return ( <main> <h1>Hello, Amplify 👋</h1> <button onClick={async () => { // create a new Todo with the following attributes const { errors, data: newTodo } = await client.models.Todo.create({ // prompt the user to enter the title content: window.prompt("title"), done: false, priority: 'medium' }) console.log(errors, newTodo); }}>Create </button> <ul> {todos.map((todo) => ( <li key={todo.id}>{todo.content}</li> ))} </ul> </main> ); }
1// pages/index.tsx
2// ...
3 return (
4 <main>
5 <h1>Hello, Amplify 👋</h1>
6 <button onClick={async () => {
7 // create a new Todo with the following attributes
8 const { errors, data: newTodo } = await client.models.Todo.create({
9 // prompt the user to enter the title
10 content: window.prompt("title"),
11 done: false,
12 priority: 'medium'
13 })
14 console.log(errors, newTodo);
15 }}>Create </button>
16
17 <ul>
18 {todos.map((todo) => (
19 <li key={todo.id}>{todo.content}</li>
20 ))}
21 </ul>
22 </main>
23 );
24}

Create a couple of to-dos, then refresh the page to see them. You can also subscribe to new to-dos in your useEffect to have them live reload on the page.

useEffect(() => { const sub = client.models.Todo.observeQuery() .subscribe(({ items }) => setTodos([...items])) return () => sub.unsubscribe() }, []);
1useEffect(() => {
2 const sub = client.models.Todo.observeQuery()
3 .subscribe(({ items }) => setTodos([...items]))
4
5 return () => sub.unsubscribe()
6}, []);

Terminate dev server

Go to localhost in the browser to make sure you can now log in and create and list to-dos. You can end your development session by shutting down the frontend dev server and cloud sandbox. The sandbox prompts you to delete your backend resources. While you can retain your backend, we recommend deleting all resources so you can start clean again next time.

Deploy and host a fullstack branch

Now that your app is working, let's deploy it to a shared fullstack branch so you can share the project with your team. Amplify offers a fully managed hosting service with CI/CD built in, making it easy to set up production and staging environments with Git branches. In Gen 2, every Git branch in your repository maps 1:1 to a fullstack branch in Amplify.

Create remote Git repository

If you already have your project remotely hosted in a Git repository, you can skip this step. Otherwise, navigate to your preferred Git provider, and add your project to a new repository. Amplify supports GitHub, AWS CodeCommit, GitLab, and Bitbucket.

Connect branch in the Amplify console

  1. To get started with Gen 2, log in to the AWS console and navigate to your preferred Region. (The Amplify console is available in 19 Regions).
  2. If you have never created apps with Amplify before, choose Create an app from the purple banner (top screenshot). If you have Amplify apps, you can find the option under New app > Build an app (Gen 2).

gen2

  1. In the Git provider screen, choose Option 2: Start with an existing app. Connect the repository you just deployed and pick a branch. Review all of your settings to ensure everything is set up correctly. Choose Save and deploy to deploy your web app to the AWS global content delivery network (CDN).
  1. That's it! Your app backend and frontend will now take ~5 minutes to build and deploy.

Manage fullstack branch

The new Amplify console gives you a central place to manage your branches, hosting settings, CI/CD builds, and backend resources. Even though you can access backend resources directly from AWS service consoles, the Amplify console offers built-in user and data administration.