✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

App16 min read

How to build a To-do App using Next.js and Strapi

August 29, 2023Updated on May 22, 2026

How to build a To-do App using Next.js and Strapi

Updated July 2023

In this article, we will learn how to use Strapi, Next.js and GraphQL to build a simple to-do app.

What is Strapi?

Strapi is the most advanced open-source Node.js Headless Content Management System used to build scalable, secure, production-ready APIs quickly and efficiently saving developers countless hours of development. With its extensible plugin system, it provides an enormous set of built-in features: Admin Panel, Authentication & Permissions management, Content Management, API Generator, etc. Strapi is 100% open-source, which means:

  • Strapi is completely free.
  • You can host it on your servers, so you own the data.
  • It is entirely customizable and extensible, thanks to the plugin system.

What is Next.js?

Next.js is a lightweight React framework to create server-rendered applications. Next.js will take care of the heavy lifting of the application build such as code splitting, HMR (hot module replacement) SSR (server-side rendering) and allow us to focus on writing the code, not our build config.

What is GraphQL?

GraphQL is a query language that allows the front end of an application to easily query an application's API. Each query requests only the data needed to be rendered by the current view. This allows the developer to craft a great user experience across multiple devices and screen sizes.

Strapi ensures that we follow best practices when creating and consuming web resources over HTTP.

For example, we have a book resource: /books. The HTTP verbs denote the action to be performed on the book’s resources. The books become a collection and a new book can be added, a book can be edited and a book can also be deleted.

The following CRUD request can be performed on the book resource using:

  • POST api/books
  • GET api/books and api/books/:id to get a specific book
  • PUT api/books/:id
  • DELETE api/books/:id

Books become a collection, and any of the CRUD actions above can be performed.

Prerequisites

To follow effectively with this article, be sure you have:

  • Node.js: Only Maintenance and LTS versions are supported (v14, v16, and v18).
    • Node v18.x is recommended for Strapi v4.3.9 and above
    • Node v16.x is recommended for Strapi v4.0.x to v4.3.8.
  • Package manager, Yarn preferably.
    • You can install yarn using npm i -g yarn, which will install yarn globally
  • Basic knowledge of NextJS
  • Code editor, Visual Studio Code preferably

Now that everything is ready, we can start building the to-do application.

Setting up Strapi

Before we can set up Strapi, we need to create a folder that will contain the source code. Open up your terminal in your desired directory and run the code below:

  mkdir strapi-todo-blog
  cd strapi-todo-blog

Next, open the strapi-todo-blog folder in Visual Studio Code. Now we can run the following lines of code in Vs Code’s integrated terminal to install Strapi.

  npx create-strapi-app@latest todo-api --quickstart

Once the installation is successful, you will get a successful message similar to the one below:

1-setting-up-strapi.png

Next, you will be redirected to the admin webpage. This is where you will create your first administrator. Fill out the requested information and click the Let’s start button.

2-setting-up-strapi.png

f you aren’t redirected and the Strapi application is not running in your terminal, then start the Strapi using yarn develop and manually Navigate to http://localhost:1337/admin/auth/register-admin

Clicking on the button will create your admin dashboard as seen below:

3-setting-up-strapi.png

Creating collections

Now, we will create the web resources. Go to Content-Types Builder and click on Create new collection type. A modal will appear, enter todo as the Display name and click on continue.

01-creating-collection.png

A modal will appear where we select the field for our collection type. Click on Text.

02-creating-collection.png

Enter todoText and click on the Finish ****button

03-creating-collection.png

04-creating-collection.png

Once you’ve created the todoText field click on save at the top right. This will cause Strapi’s server to restart. After Strapi has restarted successfully, click on Content Manager on the side nav bar, select the todo collection type and Create a new entry.

05-creating-collection.png

Next, enter a todo in the todoText, click on save and then click on publish.

06-creating-collection.png

The todo will be similar to the one below, containing TODOTEXT, CREATEDAT, UPDATEDAT, and STATE:

07-creating-collection.png

Enabling access

Strapi is designed to provide security for your application by restricting access to the various API endpoints. Making a CRUD request to the todo Collection-type without granting permission will result in a 403 Forbidden error as seen below.

01-enabling-access.png

In Strapi, there are two roles in which permission can be granted.

  • Authenticated Users: For users that are logged in.
  • Public (unauthenticated users): For users that are not logged in

    Due to the simplicity of this application, we will not develop a login system. Feel free to check out this article to create one.

To grant access:

  • Navigate to Settings then Roles & Permissions.
  • Click on the Public role.
  • Open the accordion, found in the Todo section.
  • Tick the Select all checkbox.
  • Click on Save at the top right.

02-enabling-access.png

Now, when we try to make the previous request again, we get a 200 successful message as shown:

03-enabling-access.png

Enabling GraphQL

By default, APIs generated with Strapi are REST endpoints. The endpoints can easily be integrated into GraphQL endpoints using the integrated GraphQL plugin. To install GraphQL into the application, open the todo-api folder in your terminal and run the line of code below:

npm install @strapi/plugin-graphql

Once it has been installed successfully, restart your Strapi server, navigate to http://localhost:1337/graphql, and try this query:

query Todo {
  todos {
    data {
      id
      attributes {
        todoText
      }
    }
  }
}

You should get an output similar to the one below.

04-enabling-access.png

Building the Next.js app

We have built the Strapi API and the next step is to create the frontend.

Ensure that you are in the strapi-todo-blog directory

Open your terminal in the strapi-todo-blog directory and run the code below to install NextJS.

  npx create-next-app@latest todo-app

Running the create-next-app command will ask you a few questions, follow the prompts and answer based on your preference or as shown below:

01-building-app.png

Setting up Apollo

In this tutorial, we will make use of Apollo Client to connect our NextJS application to Strapi’s graphql endpoint. Open your terminal in the todo-app folder and run the code below that installs all the dependencies needed for Apollo to work:

  npm install @apollo/client@alpha @apollo/experimental-nextjs-app-support --legacy-peer-deps

Now, create a folder in the src directory called lib and create a file in it named client.tsx Once the file has been created, we will apply the Apollo logic in the newly created file.

// src/lib/client.tsx
"use client";
import { HttpLink, SuspenseCache, ApolloLink } from "@apollo/client";
import {
  NextSSRApolloClient,
  ApolloNextAppProvider,
  NextSSRInMemoryCache,
  SSRMultipartLink,
} from "@apollo/experimental-nextjs-app-support/ssr";
const STRAPI_URL = process.env.STRAPI_URL || "http://localhost:1337";
function makeClient() {
  const httpLink = new HttpLink({
    uri: `${STRAPI_URL}/graphql`,
  });
  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache(),
    link:
      typeof window === "undefined"
        ? ApolloLink.from([
            new SSRMultipartLink({
              stripDefer: true,
            }),
            httpLink,
          ])
        : httpLink,
  });
}
function makeSuspenseCache() {
  return new SuspenseCache();
}
export function ApolloWrapper({ children }: React.PropsWithChildren) {
  return (
    <ApolloNextAppProvider
      makeClient={makeClient}
      makeSuspenseCache={makeSuspenseCache}
    >
      {children}
    </ApolloNextAppProvider>
  );
}

Above, we created an Apollo-wrapper provider. This provider will wrap the React.ReactNode (children), enabling Apollo to work on all client-side components of the application. Next, we will import the Apollo-wrapper provider into the layout.tsx.

// src/app/layout.tsx
import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { ApolloWrapper } from "@/lib/client"; //Importing the ApolloWrapper Provider
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
  title: "Todo app",
  description: "Generated by Fredrick Emmanuel",
};
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className} suppressHydrationWarning={true}>
        <ApolloWrapper>
          {/* Wrapping the React.ReactNode*/}
          {children}
        </ApolloWrapper>
      </body>
    </html>
  );
}

Feel free to check out this article for reference

Setting up the Frontend

Our todo app will look like this:

02-building-app.png

We will divide it into various components:

03-building-app.png

The Header and TodoItem section will be in a folder called components, while AddTodo and TodoList will be in containers. Create a new folder called components in the src directory and create two files: Header.tsx and TodoItem.tsx. Next, create another folder in the src directory called containers and create two files: AddTodo.tsx and Todolist.tsx. Your src directory should look like this:

📂src
┃ ┣ 📂app
┃ ┃ ┣ 📜favicon.ico
┃ ┃ ┣ 📜globals.css
┃ ┃ ┣ 📜layout.tsx
┃ ┃ ┣ 📜page.module.css
┃ ┃ ┗ 📜page.tsx
┃ ┣ 📂components
┃ ┃ ┣ 📜Header.tsx
┃ ┃ ┗ 📜TodoItem.tsx
┃ ┣ 📂containers
┃ ┃ ┣ 📜AddTodo.tsx
┃ ┃ ┗ 📜TodoList.tsx
┃ ┗ 📂lib
┃ ┃ ┗ 📜client.tsx

Let’s flesh out the files: Open the globals.css file and replace the code in it with the code below:

/* src/app/globals.css */
html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
    Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
  font-size: xx-large;
}
a {
  color: inherit;
  text-decoration: none;
}
* {
  box-sizing: border-box;
}
.main {
  padding: 10px 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.header {
  display: flex;
  justify-content: center;
  color: rgba(70, 130, 236, 1);
}
.todoInputText {
  padding: 10px 7px;
  border-radius: 3px;
  margin-right: 2px;
  margin-left: 2px;
  width: 100%;
  font-size: large;
}
button {
  padding: 10px 10px;
  border-radius: 3px;
  cursor: pointer;
  margin-right: 2px;
  margin-left: 2px;
  font-size: large;
}
.bg-default {
  background-color: rgba(70, 130, 236, 1);
  border: 1px solid rgba(28, 28, 49, 1);
  color: white;
}
.bg-danger {
  background-color: red;
  border: 1px solid rgba(28, 28, 49, 1);
  color: white;
}
.todoInputButton {
  padding: 10px 10px;
  border-radius: 3px;
  background-color: rgba(70, 130, 236, 1);
  color: white;
  border: 1px solid rgba(28, 28, 49, 1);
  cursor: pointer;
  margin-right: 2px;
  margin-left: 2px;
  font-size: large;
}
.addTodoContainer {
  margin-top: 4px;
  margin-bottom: 17px;
  width: 500px;
  display: flex;
  justify-content: space-evenly;
}
.todoListContainer {
  margin-top: 9px;
  width: 500px;
}
.todoItem {
  padding: 10px 4px;
  color: rgba(70, 130, 236, 1);
  border-radius: 3px;
  border: 1px solid rgba(28, 28, 49, 1);
  margin-top: 9px;
  margin-bottom: 2px;
  display: flex;
  justify-content: space-between;
}
.todosText {
  padding-bottom: 2px;
  border-bottom: 1px solid;
}

Next, open the Header.tsx file and add the following:

// src/components/Header.tsx
export default function Header() {
  return (
    <div className="header">
      <h2>ToDo app</h2>
    </div>
  );
}

Now we will import the Header component and place it above React.ReactNode in the layout.tsx file

// src/app/layout.tsx
import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { ApolloWrapper } from "@/lib/client"; //Importing the ApolloWrapper Provider
const inter = Inter({ subsets: ["latin"] });
import Header from "@/components/Header";

//The rest of the code

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className} suppressHydrationWarning={true}>
        <ApolloWrapper>
          {/* Wrapping the React.ReactNode*/}
          <Header />
          {/* Header */}
          {children}
        </ApolloWrapper>
      </body>
    </html>
  );
}

In the AddTodo.tsx add:

// src/container/AddTodo.tsx
import { useState } from "react";
function AddTodo({ addTodo }: { addTodo: FunctionStringCallback }) {
  const [todo, setTodo] = useState<string>("");
  return (
    <>
      <div className="addTodoContainer">
        <input
          className="todoInputText"
          type="text"
          placeholder="Add new todo here..."
          id="todoText"
          value={todo}
          onChange={(e) => {
            setTodo(e.target.value);
          }}
          onKeyDown={(e) => {
            if (e.code === "Enter") {
              addTodo(todo);
              setTodo("");
            }
          }}
        />
        <input
          className="todoInputButton"
          type="button"
          value="Add Todo"
          onClick={() => {
            addTodo(todo);
            setTodo("");
          }}
        />
      </div>
    </>
  );
}
export default AddTodo;

Based on the code above, we extracted the addTodo function from the props and called the function when Enter is hit or when the button, Add Todo is clicked. This function sends the value of the input (todo) using child-to-parent prop passing. Open the TodoItem.tsx file and add the following to it:

// src/coomponents/TodoItem.tsx
interface ItemTodo {
  todo: any;
  editTodoItem: FunctionStringCallback;
  deleteTodoItem: FunctionStringCallback;
}
function TodoItem({ todo, editTodoItem, deleteTodoItem }: ItemTodo) {
  return (
    <>
      <div className="todoItem">
        <div>{todo.attributes.todoText}</div>
        <div>
          <i>
            <button className="bg-default" onClick={() => editTodoItem(todo)}>
              Edit
            </button>
          </i>
          <i>
            <button className="bg-danger" onClick={() => deleteTodoItem(todo)}>
              Del
            </button>
          </i>
        </div>
      </div>
    </>
  );
}
export default TodoItem;

The code above displays each to-do item consisting of the todoText, Edit and Del button. Next, import the TodoItem into the TodoList.tsx file.

// src/containers/TodoList.tsx
import TodoItem from "@/components/TodoItem";
interface ListTodo {
  todos: any;
  editTodoItem: any;
  deleteTodoItem: any;
}
function TodoList({ todos, editTodoItem, deleteTodoItem }: ListTodo) {
  return (
    <div className="todoListContainer">
      <div className="todosText">Todos</div>
      {todos
        ?.sort((a: any, b: any) =>
          b.attributes.createdAt.localeCompare(a.attributes.createdAt)
        )
        .map((todo: any) => {
          return (
            <TodoItem
              todo={todo}
              key={todo.id}
              deleteTodoItem={deleteTodoItem}
              editTodoItem={editTodoItem}
            />
          );
        })}
    </div>
  );
}
export default TodoList;

This code loops through todos and displays each TodoItem. Lastly, we will import the AddTodo.tsx and TodoList.tsx file into the pages.tsx file and pass on their respective props.

// src/app/page.tsx
"use client";
import { useEffect, useState } from "react";
import AddTodo from "@/containers/AddTodo";
import TodoList from "@/containers/TodoList";
export default function Home() {
  const [todos, setTodos] = useState<[]>([]);
  const addTodo = async (todoText: string) => {};
  const editTodoItem = async (todo: any) => {
    console.log("Edited");
  };
  const deleteTodoItem = async (todo: any) => {
    console.log("Deleted");
  };
  return (
    <div>
      <main className="main">
        <AddTodo addTodo={addTodo} />
        <TodoList
          todos={todos}
          deleteTodoItem={deleteTodoItem}
          editTodoItem={editTodoItem}
        />
      </main>
    </div>
  );
}

Creating CRUD Requests

We have arrived at the main part of this tutorial. In this section, we will handle

  • Fetching all to-dos
  • Creating a to-do
  • Editing a to-do and
  • Deleting a to-do from Strapi’s Headless CMS.

Let’s Start 🎉 .

Fetching all to-dos

Create a folder in the src directory called query, create a file named schema.tsand add the following lines of code to theschema.ts` file.

// src/query/schema.ts
import { gql } from "@apollo/client";
export const GETQUERY = gql`
  {
    todos(sort: "id:desc") {
      data {
        id
        attributes {
          todoText
          createdAt
        }
      }
    }
  }
`;

Above, we will get all the newest data using sort: "id:desc"

The schema.ts file will contain the schema for all the graphql requests

Next, import GETQUERY and useQuery into the page.tsx file:

// src/app/page.tsx
"use client";
import { useEffect, useState } from "react";
import AddTodo from "@/containers/AddTodo";
import TodoList from "@/containers/TodoList";
import { useQuery } from "@apollo/experimental-nextjs-app-support/ssr";
import { GETQUERY } from "@/query/schema";

export default function Home() {
  const [todos, setTodos] = useState<[]>([]);
  const { loading, error, data } = useQuery(GETQUERY, {
    fetchPolicy: "no-cache",
  }); //Fetching all todos
  useEffect(() => {
    setTodos(data?.todos?.data); //Storing all the todos
  }, [data]);

  //rest of the code
}

Here, we fetched all the to-dos from Strapi using useQuery and stored it in the todos state. Navigate to http://localhost:3000 and you should get an out similar to the one below:

04-building-app.png

Strapi by default, sets the maximum amount of data retrieved to 10. To change this, view Strapi’s original documentation.

Creating a to-do

Before we can make a request to Strapi to add a to-do, we need to do the following:

  • Click on Content-Type Builder on the side nav bar and on the Edit button 05-building-app.png

  • Click on ADVANCED SETTINGS, uncheck Draft & Publish, and click Finish. > Strapi has a default setting that enables administrators to preview each content sent, providing them with the ability to review and assess it.

06-building-app.png

  • Next, click on the Edit icon to edit the todoText.

07-building-app.png

  • Click ADVANCE SETTINGS, tick Required field, click on the Finish button and hit the Save button at the top right.

08-building-app.png

Once this is done, we can now create a to-do. Open the schema.ts file and add the schema to add a to-do.

// src/query/schema.ts
import { gql } from "@apollo/client";

//The rest of the code

export const ADDMUT = gql`
  mutation createTodo($todoText: String) {
    createTodo(data: { todoText: $todoText }) {
      data {
        id
        attributes {
          todoText
          createdAt
        }
      }
    }
  }
`;

To add a to-do, we will import the ADDQUERY schema from schema.ts and the useMutation function from apollo/client.

// src/app/page.tsx

//The rest of the code

import { useMutation } from "@apollo/client";
import { GETQUERY, ADDMUT } from "@/query/schema";

export default function Home() {
  const [todos, setTodos] = useState<[]>([]);
  const [createTodo] = useMutation(ADDMUT);
  const { loading, error, data } = useQuery(GETQUERY, {
    fetchPolicy: "no-cache",
  }); //Fetching all todos
  useEffect(() => {
    console.log(data?.todos?.data);
    setTodos(data?.todos?.data); //Storing all the todos
  }, [data]);
  const addTodo = async (todoText: string) => {
    await createTodo({
      //Creating a new todo
      variables: {
        todoText: todoText, //Passing the todo text
      },
    }).then(({ data }: any) => {
      setTodos([...todos, data?.createTodo?.data] as any); //Adding the new todo to the list
    });
  };

  //The rest of the code.
}

Editing a to-do

To update data in Strapi using Graphql, we will make use of the id of the particular todo we are updating. Open the schema.ts file and add the graphql mutation for the update functionality.

// src/query/schema.ts
import { gql } from "@apollo/client";

//The rest of the code

export const UPDATEMUT = gql`
  mutation updateTodo($id: ID!, $todoText: String!) {
    updateTodo(id: $id, data: { todoText: $todoText }) {
      data {
        id
        attributes {
          todoText
          createdAt
        }
      }
    }
  }
`;

Now, import the UPDATEMUT schema into the pages.tsx file.

// src/app/page.tsx
//The rest of the code

import { GETQUERY, ADDMUT, UPDATEMUT } from "@/query/schema";
export default function Home() {
  const [todos, setTodos] = useState<[]>([]);
  const [createTodo] = useMutation(ADDMUT);
  const [updateTodo] = useMutation(UPDATEMUT);

  //The rest of the code

  const editTodoItem = async (todo: any) => {
    const newTodoText = prompt("Enter new todo text or description:");
    if (newTodoText != null) {
      await updateTodo({
        //updating the todo
        variables: {
          id: todo.id,
          todoText: newTodoText,
        },
      }).then(({ data }: any) => {
        const moddedTodos: any = todos.map((_todo: any) => {
          if (_todo.id === todo.id) {
            return data?.updateTodo?.data;
          } else {
            return _todo;
          }
        });
        setTodos(moddedTodos);
      });
    }
  };

  //The rest of the code
}

The editTodoItem function edits a todo. It prompts the user to enter the new todo text. Upon clicking OK in the prompt dialogue, it edits the todo with the todo id.

Deleting a to-do

We’ve come to the last section in our CRUD request. In this section, we will delete a to-do using the to-do id. Open the schema.ts file and add DELETEMUT.

// src/query/schema.ts
import { gql } from "@apollo/client";

//The rest of the code

export const DELETEMUT = gql`
  mutation deleteTodo($id: ID!) {
    deleteTodo(id: $id) {
      data {
        id
        attributes {
          todoText
          createdAt
        }
      }
    }
  }
`;

Import the DELETEMUT schema into pages.tsx.

// src/app/page.tsx
//The rest of the code

import { GETQUERY, ADDMUT, UPDATEMUT, DELETEMUT } from "@/query/schema";
export default function Home() {
  const [todos, setTodos] = useState<[]>([]);
  const [createTodo] = useMutation(ADDMUT);
  const [updateTodo] = useMutation(UPDATEMUT);
  const [deleteMUT] = useMutation(DELETEMUT);

  //The rest of the code
  const deleteTodoItem = async (todo: any) => {
    if (confirm("Do you really want to delete this item?")) {
      await deleteMUT({
        //Deleting the todo
        variables: {
          id: todo.id,
        },
      }).then(({ data }: any) => {
        const newTodos = todos.filter((_todo: any) => _todo.id !== todo.id);
        setTodos(newTodos as any);
      });
    }
  };
  //The rest of the code
}

Get the full source code from the resources section

The deleteTodoItem function accepts the todo object to be deleted in the todo arg. It confirms first if the user wants to delete the to-do, if yes, it proceeds to delete the to-do. This will cause Strapi to delete the todo with the id in its database. Next, we filter out the deleted todo and set the filtered result as the new todos in the todos state.

Resources

You can find the source code of the application here:

You can also decide to read more on Strapi and graphql using the links:

Conclusion

Hurray 🎉 , we’ve come to the end of this article. The article shows how to integrate Strapi into NextJS by building a to-do app. Feel free to check out the app on your various machines and comment on any errors, if encountered.

Fredrick EmmanuelFull Stack Web Developer

Full Stack Web Developer. Loves JavaScript.

Join our Newsletter

Get all the latest news and events.

Newsletter signup is not configured yet.

App·26 min read

Build a To-Do App with Strapi GraphQL Plugin and Flutter

In this tutorial, we will set up a GraphQL endpoint in a Strapi backend along with Flutter, a powerful opensource UI...

·August 30, 2023
How to setup Amazon S3 Upload Provider Plugin for your Strapi App
AppBeginner·14 min read

How to Set up Amazon S3 Upload Provider Plugin for Your Strapi App

Learn how to set up the Amazon S3 Upload Provider Plugin for your Strapi App to work with an Amazon S3 storage bucket.

·July 3, 2023
Create a Food Ordering App with Strapi and Next.js (1/7)
AppIntermediate·9 min read

Create a Food Ordering App with Strapi and Next.js 1/7

Learn how to create a food ordering app with Strapi and Next.js using React Hooks.

·June 28, 2023