Build a Todo App with Next.js and AWS Amplify

Build a Todo App with Next.js and AWS Amplify

Introduction

In this tutorial, you'll learn how to build a serverless todo application using Next.js, a popular React framework for building server-rendered applications, and AWS Amplify, a JavaScript library for cloud-based app development.

Prerequisites

Before getting started, you will need to have the following tools installed on your development machine:

  • Node.js (version 12 or higher)

  • npm (comes with Node.js)

  • AWS Amplify CLI

You can learn how to setup the AWS Amplify CLI here.

Step 1 - Set Up Next.js App

To create a new Next.js app, run the following command:

npx create-next-app todo-app

This will create a new directory called todo-app with Next.js project dependencies.

Next, navigate to the todo-app directory and start the development server by running the following command:

cd todo-app
npm run dev

This will start the development server at http://localhost:3000. You should see the default Next.js welcome page.

Step 2 - Initialize AWS Amplify

Next, initialize AWS Amplify in your Next.js project root folder by running the following command:

amplify init -y

Amplify Setup

Step 3 - Configure Amplify API

Once Amplify has been initialized, add a backend service to the app. For this tutorial, we will use a GraphQL API with AWS AppSync. To add the API, run the following command:

amplify add api

Follow the prompts to create a new GraphQL API with AWS AppSync. When asked for the type of API, choose GraphQL. This will create a schema.graphql file in the amplify/backend/api/todoapp directory.

Add API

Step 4 - Update Todo Schema

After setting up the API, update the todo app GraphQL schema to specify the data types:

//schema.graphql
type Todo @model {
  id: ID!
  title: String!
  completed: Boolean!
}

The code above defines a Todo type with fields for the todo title, and completion status.

Now, push the changes made to the Amplify backend by running:

amplify push

Amplify Push

Step 5 - Build the Todo UI

Now that you've fully configured the API, the next step is to build out the frontend of the application.

First, connect Amplify to the Next.js app by installing Amplify library:

npm install aws-amplify

Then, create a components folder in the root of the application which will consist of these three components: CreateTodo.jsx, Todos.jsx and TodoItem.jsx.

In order to follow along with the styling used in this application, add the Tailwind CSS & Fontawesome CDN link to the Head tag of your pages/_document.js.

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" />

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" />

Next, update the pages/index.js file as shown below:

//index.js

import Head from "next/head";
import React, { useState } from "react";

import { Amplify, API } from 'aws-amplify';
import awsconfig from '../src/aws-exports';

import * as mutations from "../src/graphql/mutations";
import * as queries from '../src/graphql/queries';

Amplify.configure(awsconfig);

export default function Home() {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main>
        <h1>Hello World</h1>
      </main>
    </>
  );
}

Fetch Todo Data

Using Next.js getStaticProps functionality, let's fetch the todo data from the Amplify backend:

//index.js
export async function getStaticProps() {
  const todoData = await API.graphql({
    query: queries.listTodos,
  });

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

export default function Home({ todos }) {
  const [todoList, setTodoList] = useState(todos);
    ...
}

In the code above, we used Amplify's automatically generated query listTodos imported from the ../src/graphql/queries directory to get a list of all the Todos from the backend API, which is then passed as props to the main component.

Create Todo

In the index.js file, write a onCreateTodo function that will be called when a todo is created by the user:

//index.js
import CreateTodo from "../components/CreateTodo";

export default function Home({ todos }) {
...
const onCreateTodo = async (todo) => {
    const newTodo = {
      title: todo,
      completed: false,
    };

    try {
      await API.graphql({
        query: mutations.createTodo,
        variables: { input: newTodo },
      });

      setTodoList((list) => [...list, { ...newTodo }]);

      console.log("Successfully created a todo!");
    } catch (err) {
      console.log("error: ", err);
    }
  };

return (
    ...
    <CreateTodo onCreateTodo={onCreateTodo} />
    ...
  )
}

Here, the onCreateTodo function receives an input of an object with title and completed values. Also, this function is passed to the CreateTodo component as a prop

Next, let's implement the CreateTodo component UI:

//CreateTodo.jsx
import React, { useState } from "react";

const CreateTodo = ({ onCreateTodo }) => {
  const [todoItem, setTodoItem] = useState("");

  const onInputChange = (e) => {
    const { value } = e.target;
    setTodoItem(value);
  };

  const onSubmit = (e) => {
    e.preventDefault();
    onCreateTodo(todoItem)
    setTodoItem("");
  };

  return (
    <>
      <form className="flex justify-center mt-10">
        <div className="bg-yellow-300 px-4 py-2 rounded-lg w-96">
          <h1 className="text-center mt-4 mb-4 text-2xl text-white font-bold">
            TodoList
          </h1>
          <div className="mt-6 flex space-x-4 m-10 justify-center">
            <input
              className="bg-gray-100 rounded-md py-2 px-4 border-2 outline-none"
              id="todo"
              name="todo"
              value={todoItem}
              placeholder="Create a new Todo"
              onChange={onInputChange}
            />
            <button
              className="bg-black text-white px-4 rounded-md font-semibold"
              onClick={onSubmit}
            >
              <i className="fa-solid fa-plus"></i>
            </button>
          </div>
        </div>
      </form>
    </>
  );
};

export default CreateTodo;

Delete Todo

To delete a todo, add a onDeleteTodo function to the index.js file that deletes a todo based on its 'id' field and then pass it as a prop to the Todos.jsx component:

//index.js
import Todo from "../components/Todos";

export default function Home({ todos }) {
...
const onDeleteTodo = async (id) => {
    try {
      await API.graphql({
        query: mutations.deleteTodo,
        variables: { input: { id } },
      });

      const filterTodo = todoList.filter((todo) => todo.id !== id);
      setTodoList(filterTodo);

      console.log("Successfully deleted a todo!");
    } catch (err) {
      console.log({ err });
    }
  };

return (
    ...
     <Todo todoList={todoList} onDeleteTodo={onDeleteTodo} />
    ...
  )
}

Note that the onDeleteTodo function will be implemented on a delete button for each todo items later on in the tutorial

Display Todos Component

Next, we will implement the UI for displaying the list of todos. Update the Todos.jsx with the following content:

//Todos.jsx
import React from "react";
import TodoItem from "./TodoItem";

const Todo = ({ todoList, onDeleteTodo }) => {
  return (
    <>
      <div className="h-screen">
        <div>
          {todoList.map((todo) => (
            <TodoItem
              key={todo.id}
              todo={todo}
              onDeleteTodo={onDeleteTodo}
            />
          ))}
        </div>
      </div>
    </>
  );
};

export default Todo;

From the code above, we iterated through the array of todos which will then display each todo as a TodoItem component.

Update Todo

In the TodoItem.jsx component, let's create a onUpdateTodo function that uses the API's updateTodo mutation imported from the ../src/graphql/mutations directory to allow users mark a todo as completed or not:

//TodoItem.jsx
import React, { useState } from "react";
import { API } from "aws-amplify";
import * as mutations from "../src/graphql/mutations";

const onUpdateTodo = async (event) => {
    setIsCompleted(event.target.checked);
    const input = { id: todo.id, completed: event.target.checked };
    try {
      await API.graphql({
        query: mutations.updateTodo,
        variables: {
          input: { input },
        },
      });

      console.log("Todo successfully updated!");
    } catch (err) {
      console.log("error: ", err);
    }
  };

Display Todo Items

Let's update the TodoItem component so we can display each todo item with a working checkbox and delete icon:

//TodoItem.jsx
const TodoItem = ({ todo, onDeleteTodo }) => {
  const [isCompleted, setIsCompleted] = useState(todo.completed);

  ...
  return (
    <>
      <div className="flex justify-center">
        <div className=" relative justify-center mt-6 w-96">
          <div className="bg-white px-8 pt-8 pb-6 rounded-md text-gray-500 shadow-lg">
            <div
              key={todo.id}
              style={{
                textDecoration: isCompleted ? "line-through" : undefined,
              }}
            >
              <input
                type="checkbox"
                checked={isCompleted}
                onChange={OnUpdateTodo}
                className="h-4 w-4"
              />
              <span className="pl-3">{todo.title}</span>
            </div>
          </div>
          <div className="absolute flex top-5 right-0 p-3 space-x-2">
            <span
              className="text-red-500 cursor-pointer"
              onClick={() => onDeleteTodo(todo.id)}
            >
              <i className="fa-solid fa-trash"></i>
            </span>
          </div>
        </div>
      </div>
    </>
  );
};

export default TodoItem;

Step 6 - Deploy the App

Now that we have our todo app implemented, we can deploy it to the cloud.

To deploy the app, run the following command:

amplify add hosting

Then, run:

amplify publish

This will build the app and deploy it to AWS. When the deployment is complete, you will see a URL where you can access the app.

Conclusion

In this tutorial, we have successfully built a serverless todo app using AWS Amplify and Next.js. We defined a GraphQL API using AWS AppSync. Finally, we deployed the app to the cloud using AWS Amplify.

I hope you have found this tutorial helpful! You can find the full source code in this Github repo and the demo here