Get Started With AWS Amplify, Next.JS & Typescript. Full Stack with GraphQL + Authentication in 10 minutes!

Build a full-stack authenticated to-do list application using modern serverless frameworks like Next.JS and AWS Amplify.

Featured on Hashnode

Introduction

This post aims to guide you on how to get started with AWS Amplify, Next.JS and Typescript, in about 10 minutes.

What we are going to set up is a full-stack, serverless web application using the most modern technologies (in my opinion), we'll be creating:

  • A Structured, schema-based database hosted on AWS DynamoDB
  • A GraphQL API hosted on AWS AppSync
  • A basic authentication system using AWS Cognito
  • A basic Todo web application to demonstrate the front-end

If you're a video lover, you can watch this article instead here:

Guide

Create the Next App

To create a new Next.JS application, run npx create-next-app <your-app-name>.

If you aren't familiar with creating apps like this, this command enables you to quickly start building a new Next.js application, with everything set up for you. All you need installed is npx (Which is bundled in with npm these days)!

After that is complete, I like to re-arrange the folder structure a little bit from the default, I prefer to:

  1. Delete the api folder
  2. Create a src folder
  3. Move the pages folder into the newly created src folder
  4. Delete the _app.js file for now

Setting up Typescript

Setting up TypeScript in Next JS is really easy.

Next.js provides an integrated TypeScript experience out of the box, similar to an IDE.

To start using Typescript, create a tsconfig.json file at the root of your directory; meaning the most-outer folder of the project.

What is that?

The presence of a tsconfig.json file in a directory indicates that the directory is the root of a TypeScript project. The tsconfig.json file specifies the root files and the compiler options required to compile the project.

With the presence of a tsconfig.json file, Next JS now knows you're trying to use Typescript. Try running the npm run dev command in this project.

If we've set everything up correctly, it'll give you an error saying you need to install some packages.

  • typescript
  • @types/node
  • @types/react

Run npm install --save-dev typescript @types/react @types/node

or yarn add --dev typescript @types/react @types/node for those using Yarn.

Now try running npm run dev again. You should see a nice message saying

We detected TypeScript in your project and created a tsconfig.json file for you.

Check it out, open up tsconfig.json again - you'll notice this is filled out with a bunch of things now (hopefully)!

One thing I like to change about this config is the strict mode option from false to true. But once again, that is a personal preference. It'll likely depend on how comfortable you are with TypeScript.

Now we can start swapping our code over to Typescript. So, all we need to do is change our index.js to be an index.tsx and we are Typescript-ready.

Setting up AWS Amplify

Before we begin setting up with the AWS Amplify CLI, you'll want to create an AWS Account if you haven't done so already.

You can do that here: Create AWS Account

Next thing you'll need to do is set-up the AWS Amplify CLI.

Nader Dabit has a quick 2 minute guide on how to do that here:

Initializing AWS Amplify in our Project

To initialise a new AWS Amplify project, simply run the amplify init command at the root of your project.

Below, you'll find the answers to the configuration options the Amplify CLI will ask us, these are the options we want to use for this tutorial. (You can change most of them later).

# <Interactive>
? Enter a name for the project <PROJECT_NAME>
? Enter a name for the environment: dev (or whatever you would like to call this env)
? Choose your default editor: <YOUR_EDITOR_OF_CHOICE>
? Choose the type of app that you're building (Use arrow keys)
  android
  ios
 javascript
? What javascript framework are you using react
? Source Directory Path:  src
? Distribution Directory Path: build
? Build Command:  npm run build
? Start Command: npm run start
? Do you want to use an AWS profile? Y
? Select the authentication method you want to use: AWS Profile
? Please choose the profile you want to use: <Your profile

# </Interactive>

Things to note:

  • We are using src as the source directory path. The src folder is optional in Next JS, but I like to create it.
  • If you open your text editor of choice, you'll see a bunch of files have been created. (They look a bit scary and complex, but we will explain them later.)

Adding Amplify API and Amplify Auth

If you're like me you like to know what's going on behind the scenes. Before we start adding our API and Authenticstion with the amplify add command, I will quickly explain what we are doing first at a high level.

When you run the amplify add command, you are actually creating a resource on an AWS service.

amplify add api is creating a hosted GraphQL API on AWS AppSync. It is also creating an AWS DynamoDB to store your data in, based on a schema we are going to make.

amplify add auth is creating an AWS Cognito service. This auth is a bit complex in my opinion, and I would recommend reading some documentation if you'd like to know more.

We are going to create all of these services with only one command. We are going to create an API with specified authentication rules, as outlined below:

  • Users can create a Todo
  • Users can update and delete their own Todos
  • Users can read other users Todos
  • Guests (users without accounts) can read other users todos

Obviously, in order to distinguish between guests and users, we will need a way of creating accounts.

Run amplify add api in your project and follow the options configured below:

# <Interactive>
? Please select from one of the below mentioned services (Use arrow keys)
❯ GraphQL
  REST
? Provide API name: <API_NAME>
? Choose the default authorization type for the API (Use arrow keys)
❯ API key
  Amazon Cognito User Pool
  IAM
  OpenID Connect
? Enter a description for the API key: <API_DESCRIPTION>
? After how many days from now the API key should expire (1-365): 7
? Do you want to configure advanced settings for the GraphQL API:
  No, I am done.
❯ Yes, I want to make some additional changes.
? Configure additional auth types? y
? Choose the additional authorization types you want to configure for the API
❯(*) Amazon Cognito User Pool
 ( ) IAM
 ( ) OpenID Connect
Do you want to use the default authentication and security configuration? (Use arrow keys)
❯ Default configuration
  Default configuration with Social Provider (Federation)
  Manual configuration
  I want to learn more.
How do you want users to be able to sign in? (Use arrow keys)
  Username
❯ Email
  Phone Number
  Email or Phone Number
  I want to learn more.
Do you want to configure advanced settings? (Use arrow keys)
❯ No, I am done.
  Yes, I want to make some additional changes.
? Enable conflict detection? N
? Do you have an annotated GraphQL schema? N
? Choose a schema template: (Use arrow keys)
❯ Single object with fields (e.g., “Todo” with ID, name, description)
  One-to-many relationship (e.g., “Blogs” with “Posts” and “Comments”)
  Objects with fine-grained access control (e.g., a project management app with owner-based authorization)
? Do you want to edit the schema now? Y
# </Interactive>

Things to note:

  • We have just created a schema for our database (which we can now edit)
  • We have also created an authentication service
  • We have defined two different ways of interacting with our API: API Key (guest) and using Amazon Cognito User Pools (users).

If everything went to plan, you should now be able to edit your GraphQL schema.

If you don't already have it open, it's located at /amplify/backend/api/<project name>/schema.graphql

Let's edit our schema a little bit, change this file to the following:

type Todo
  @model
  @auth(
    rules: [
      { allow: owner } # Allow the creator of a todo to perform Create, Update, Delete operations.
      { allow: public, operations: [read] } # Allow public (guest users without an account) to Read todos.
      { allow: private, operations: [read] } # Allow private (other signed in users) to Read todos.
    ]
  ) {
  id: ID!
  name: String!
  description: String
}

What does this do?

  • Creates a todo schema containing an ID (auto-generated), name, and description.
  • Allows users to create todos, and allows the owner to be able to create and delete their own todos.
  • Guests and other users can view all todos.

Publishing Our Changes to the Cloud

Run amplify push to publish our changes to the cloud.

Accept the following configuration options when pushing:

# <Interactive>
? Are you sure you want to continue? Y
? Do you want to generate code for your newly created GraphQL API? Y
? Choose the code generation language target (Use arrow keys)
  javascript
❯ typescript
  flow
? Enter the file name pattern of graphql queries, mutations and subscriptions (src/graphql/**/*.ts)
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions (Y/n) Y
? Enter maximum statement depth [increase from default if your schema is deeply nested] (2)
? Enter the file name for the generated code: src\API.ts
# </Interactive>

Here is where the magic of AWS Amplify happens. It is going to generate all CRUD query and mutations possible, based on your schema.

For example, it is going to generate listTodos, createTodo, updateTodo, deleteTodo.

Not only that, it is going to generate Typescript types for each of these queries and mutations. Super powerful!

Viewing Our Created Resources

To browse around with what exactly we have pushed to the cloud, run amplify console.

Select Amplify Console and you should see a dashboard of your project. Have a play around and browse the behind the scenes of the API and Authentication.

Creating a home page to list our todos

To start connecting our front end, we first need to install two packages. Run

npm install aws-amplify @aws-amplify/ui-react

Ready to go!

We are going to create a home page, which will list all of our todos, and have a form to create a new todo.

Let's break this page up into four sections.

For a more pleasant viewing, you can view the full code for this file here:

0. The Import statements & Amplify.Configure

// index.tsx
import Amplify, { API, withSSRContext } from "aws-amplify";
import { GetServerSideProps } from "next";
import React from "react";
import styles from "../../styles/Home.module.css";
import { Todo, CreateTodoInput, CreateTodoMutation, ListTodosQuery } from "../API";
import { createTodo } from "../graphql/mutations";
import { listTodos } from "../graphql/queries";
import { GRAPHQL_AUTH_MODE } from "@aws-amplify/api";
import { AmplifyAuthenticator } from "@aws-amplify/ui-react";
import awsExports from "../aws-exports";
import { useRouter } from "next/router";

Amplify.configure({ ...awsExports, ssr: true });

1. getServersideProps

Next.JS comes with a function called getServersideProps. It's code that you include in the same file as a typical page function, that gets run on the server-side. Anything you return from this function gets passed to the main function as props.

If you export an async function called getServerSideProps from a page, Next.js will pre-render this page on each request using the data returned by getServerSideProps.

// index.tsx
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
  const SSR = withSSRContext({ req });

  const response = (await SSR.API.graphql({
    query: listTodos,
    authMode: GRAPHQL_AUTH_MODE.API_KEY,
  })) as {
    auth;
    data: ListTodosQuery;
  };

  return {
    props: {
      todos: response.data.listTodos.items,
    },
  };
};

Here we are using getServersideProps along with withSSRContext from AWS Amplify to create a serverside request to our GraphQL API. We are using the auto-generated request listTodos to grab all todos that exist in the database.

Notice how we are typing the data from the response as ListTodosQuery which is an auto-generated type from the AWS Amplify CLI.

Then we are returning the todos in the props object, which will be given to our main function.

2 and 3: The main function and the internal createTodo function

// index.tsx
export default function Home({ todos = [] }: { todos: Todo[] }) {
  const router = useRouter()

  async function handleCreateTodo(event) {
    event.preventDefault();

    const form = new FormData(event.target);

    try {
      const createInput: CreateTodoInput = {
        name: form.get("title").toString(),
        description: form.get("content").toString(),
      };

      const request = (await API.graphql({
        authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS,
        query: createTodo,
        variables: {
          input: createInput,
        },
      })) as { data: CreateTodoMutation; errors: any[] };

      router.push(`/todo/${request.data.createTodo.id}`)


    } catch ({ errors }) {
      console.error(...errors);
      throw new Error(errors[0].message);
    }
  }

  return (
    <div className={styles.container}>
      <div className={styles.grid}>
        {todos.map((todo) => (
          <a href={`/todo/${todo.id}`} key={todo.id}>
            <h3>{todo.name}</h3>
            <p>{todo.description}</p>
          </a>
        ))}
      </div>

      <AmplifyAuthenticator>
        <form onSubmit={handleCreateTodo}>
          <fieldset>
            <legend>Title</legend>
            <input defaultValue={`Today, ${new Date().toLocaleTimeString()}`} name="title" />
          </fieldset>

          <fieldset>
            <legend>Content</legend>
            <textarea defaultValue="I built an Amplify app with Next.js!" name="content" />
          </fieldset>

          <button>Create Todo</button>
        </form>
      </AmplifyAuthenticator>
    </div>
  );
}

Notice how we are wrapping the form to create a new todo in an AmplifyAuthenticator. This is part of the @aws-amplify/ui-react package. It requires a user to be logged in to view the children within it.

Creating a dynamic route to show individual todo in detail

This page will take advantage of Next JS's Dynamic routes and Static Site Generation (SSR).

Create a folder inside the pages folder called todo. Within this new folder, create a file called [id].tsx. This page is now going to capture all pages where the user visits any route that is like /todo/<something> in their web browser.

At build time, we are going to statically generate an individual page for each todo that exists using Next JS's getStaticPaths and getStaticProps

Here is what that looks like:

// [id].tsx
export const getStaticPaths: GetStaticPaths = async () => {
  const SSR = withSSRContext();

  const todosQuery = (await SSR.API.graphql({
    query: listTodos,
    authMode: GRAPHQL_AUTH_MODE.API_KEY,
  })) as { data: ListTodosQuery; errors: any[] };

  const paths = todosQuery.data.listTodos.items.map((todo: Todo) => ({
    params: { id: todo.id },
  }));

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

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const SSR = withSSRContext();

  const response = (await SSR.API.graphql({
    query: getTodo,
    variables: {
      id: params.id,
    },
  })) as { data: GetTodoQuery };

  return {
    props: {
      todo: response.data.getTodo,
    },
  };
};

Here, we are using getStaticPaths to fetch all of our todos with a pre-built query listTodos and storing it in the todosQuery variable. Then, we extract all the ids from the todos, and use them as the paths that we return from the getStaticPaths function.

Now, Next.JS is going to statically pre-render all the paths that we specified; in this case, all of the ids of all our todos.

Next, we are using getStaticProps to provide the individual information for a todo. Next.js will pre-render each page at build time that we returned from our getStaticPaths function with the information we provide here. Similarly to getServerSideProps, we are also passing props from this return to our main function.

So, we return the information of a specific todo by it's id with the pre-built query getTodo, and pass it to the main function as props:

// [id].tsx
import { Amplify, API, withSSRContext } from "aws-amplify";
import Head from "next/head";
import { useRouter } from "next/router";
import { DeleteTodoInput, GetTodoQuery, Todo, ListTodosQuery } from "../../API";
import awsExports from "../../aws-exports";
import { deleteTodo } from "../../graphql/mutations";
import { getTodo, listTodos } from "../../graphql/queries";
import { GetStaticProps, GetStaticPaths } from "next";
import { GRAPHQL_AUTH_MODE } from "@aws-amplify/api";
import styles from "../../../styles/Home.module.css";

Amplify.configure({ ...awsExports, ssr: true })

export default function TodoPage({ todo }: { todo: Todo }) {
  const router = useRouter();

  if (router.isFallback) {
    return (
      <div className={styles.container}>
        <h1 className={styles.title}>Loading&hellip;</h1>
      </div>
    );
  }

  async function handleDelete(): Promise<void> {
    try {
      const deleteInput: DeleteTodoInput = {
        id: todo.id,
      };

      await API.graphql({
        authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS,
        query: deleteTodo,
        variables: {
          input: deleteInput,
        },
      });

      router.push(`/`);
    } 
    catch ({ errors }) {
      console.error(...errors);
      throw new Error(errors[0].message);
    }
  }

  return (
    <div className={styles.container}>
      <Head>
        <title>{todo.name} – Amplify + Next.js</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>{todo.name}</h1>
        <p className={styles.description}>{todo.description}</p>
      </main>

      <button className={styles.footer} onClick={handleDelete}>
        💥 Delete todo
      </button>
    </div>
  );
}

We are also created a handleDelete async function. This function uses another auto-generated GraphQL query from AWS Amplify. You may notice we are passing the authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS here too, which means we are sending the user information along with the request so that AWS Amplify can use it to compare against our authorization rules on the backend.

For example, if we were not the owner of this todo, Amplify would prevent us from deleting it according to our rules we set up earlier.

We're also using Next.JS's useRouter hook to navigate the user back to the home page when they delete the todo.

Conclusion

Thanks for taking the time to read my small piece!

I believe this is one of the most powerful tech stacks we have in the serverless space currently, but it can be a little intimidating to get into at first, and the Amplify documentation is sometimes a bit rough.

I hope you enjoyed.

Interested in reading more such articles from Jarrod Watts?

Support the author by donating an amount of your choice.

Recent sponsors
Margaret Goldman's photo

Great guide! Thanks for sharing. 🙌

Jarrod Watts's photo

Thanks Margaret!

Catalin Pit's photo

Great article & video! I followed you on Hashnode and subscribed to your YouTube channel!

Jarrod Watts's photo

Thank you so much Catalin! I've done the same for you :D

Rohit's photo

Hi Jarrod Watts,

Your videos are really awesome. Do you help building projects? if so please let me know I'd like to connect with you.