Updated July 2023
In this article, we will learn how to use Strapi, Next.js and GraphQL to build a simple to-do app.
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:
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.
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:
api/books
api/books
and api/books/:id
to get a specific bookapi/books/:id
api/books/:id
Books become a collection, and any of the CRUD actions above can be performed.
To follow effectively with this article, be sure you have:
v14
, v16
, and v18
).v4.3.9
and abovev4.0.x
to v4.3.8
.npm i -g yarn
, which will install yarn globallyNow that everything is ready, we can start building the to-do application.
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:
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.
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:
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.
A modal will appear where we select the field for our collection type.
Click on Text
.
Enter todoText
and click on the Finish ****button
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.
Next, enter a todo in the todoText, click on save and then click on publish.
The todo will be similar to the one below, containing TODOTEXT, CREATEDAT, UPDATEDAT, and STATE:
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.
In Strapi, there are two roles in which permission can be granted.
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:
Public
role.Todo
section.Select all
checkbox.Now, when we try to make the previous request again, we get a 200
successful message as shown:
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:
1npm install @strapi/plugin-graphql
Once it has been installed successfully, restart your Strapi server, navigate to http://localhost:1337/graphql, and try this query:
1query Todo {
2 todos {
3 data {
4 id
5 attributes {
6 todoText
7 }
8 }
9 }
10}
You should get an output similar to the one below.
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:
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.
1// src/lib/client.tsx
2"use client";
3import { HttpLink, SuspenseCache, ApolloLink } from "@apollo/client";
4import {
5 NextSSRApolloClient,
6 ApolloNextAppProvider,
7 NextSSRInMemoryCache,
8 SSRMultipartLink,
9} from "@apollo/experimental-nextjs-app-support/ssr";
10const STRAPI_URL = process.env.STRAPI_URL || "http://localhost:1337";
11function makeClient() {
12 const httpLink = new HttpLink({
13 uri: `${STRAPI_URL}/graphql`,
14 });
15 return new NextSSRApolloClient({
16 cache: new NextSSRInMemoryCache(),
17 link:
18 typeof window === "undefined"
19 ? ApolloLink.from([
20 new SSRMultipartLink({
21 stripDefer: true,
22 }),
23 httpLink,
24 ])
25 : httpLink,
26 });
27}
28function makeSuspenseCache() {
29 return new SuspenseCache();
30}
31export function ApolloWrapper({ children }: React.PropsWithChildren) {
32 return (
33 <ApolloNextAppProvider
34 makeClient={makeClient}
35 makeSuspenseCache={makeSuspenseCache}
36 >
37 {children}
38 </ApolloNextAppProvider>
39 );
40}
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
.
1// src/app/layout.tsx
2import "./globals.css";
3import type { Metadata } from "next";
4import { Inter } from "next/font/google";
5import { ApolloWrapper } from "@/lib/client"; //Importing the ApolloWrapper Provider
6const inter = Inter({ subsets: ["latin"] });
7export const metadata: Metadata = {
8 title: "Todo app",
9 description: "Generated by Fredrick Emmanuel",
10};
11export default function RootLayout({
12 children,
13}: {
14 children: React.ReactNode;
15}) {
16 return (
17 <html lang="en">
18 <body className={inter.className} suppressHydrationWarning={true}>
19 <ApolloWrapper>
20 {/* Wrapping the React.ReactNode*/}
21 {children}
22 </ApolloWrapper>
23 </body>
24 </html>
25 );
26}
Feel free to check out this article for reference
Our todo app will look like this:
We will divide it into various components:
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:
1📂src
2┃ ┣ 📂app
3┃ ┃ ┣ 📜favicon.ico
4┃ ┃ ┣ 📜globals.css
5┃ ┃ ┣ 📜layout.tsx
6┃ ┃ ┣ 📜page.module.css
7┃ ┃ ┗ 📜page.tsx
8┃ ┣ 📂components
9┃ ┃ ┣ 📜Header.tsx
10┃ ┃ ┗ 📜TodoItem.tsx
11┃ ┣ 📂containers
12┃ ┃ ┣ 📜AddTodo.tsx
13┃ ┃ ┗ 📜TodoList.tsx
14┃ ┗ 📂lib
15┃ ┃ ┗ 📜client.tsx
Let’s flesh out the files:
Open the globals.css
file and replace the code in it with the code below:
1/* src/app/globals.css */
2html,
3body {
4 padding: 0;
5 margin: 0;
6 font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
7 Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
8 font-size: xx-large;
9}
10a {
11 color: inherit;
12 text-decoration: none;
13}
14* {
15 box-sizing: border-box;
16}
17.main {
18 padding: 10px 0;
19 flex: 1;
20 display: flex;
21 flex-direction: column;
22 justify-content: center;
23 align-items: center;
24}
25.header {
26 display: flex;
27 justify-content: center;
28 color: rgba(70, 130, 236, 1);
29}
30.todoInputText {
31 padding: 10px 7px;
32 border-radius: 3px;
33 margin-right: 2px;
34 margin-left: 2px;
35 width: 100%;
36 font-size: large;
37}
38button {
39 padding: 10px 10px;
40 border-radius: 3px;
41 cursor: pointer;
42 margin-right: 2px;
43 margin-left: 2px;
44 font-size: large;
45}
46.bg-default {
47 background-color: rgba(70, 130, 236, 1);
48 border: 1px solid rgba(28, 28, 49, 1);
49 color: white;
50}
51.bg-danger {
52 background-color: red;
53 border: 1px solid rgba(28, 28, 49, 1);
54 color: white;
55}
56.todoInputButton {
57 padding: 10px 10px;
58 border-radius: 3px;
59 background-color: rgba(70, 130, 236, 1);
60 color: white;
61 border: 1px solid rgba(28, 28, 49, 1);
62 cursor: pointer;
63 margin-right: 2px;
64 margin-left: 2px;
65 font-size: large;
66}
67.addTodoContainer {
68 margin-top: 4px;
69 margin-bottom: 17px;
70 width: 500px;
71 display: flex;
72 justify-content: space-evenly;
73}
74.todoListContainer {
75 margin-top: 9px;
76 width: 500px;
77}
78.todoItem {
79 padding: 10px 4px;
80 color: rgba(70, 130, 236, 1);
81 border-radius: 3px;
82 border: 1px solid rgba(28, 28, 49, 1);
83 margin-top: 9px;
84 margin-bottom: 2px;
85 display: flex;
86 justify-content: space-between;
87}
88.todosText {
89 padding-bottom: 2px;
90 border-bottom: 1px solid;
91}
Next, open the Header.tsx
file and add the following:
1// src/components/Header.tsx
2export default function Header() {
3 return (
4 <div className="header">
5 <h2>ToDo app</h2>
6 </div>
7 );
8}
Now we will import the Header
component and place it above React.ReactNode
in the layout.tsx
file
1// src/app/layout.tsx
2import "./globals.css";
3import type { Metadata } from "next";
4import { Inter } from "next/font/google";
5import { ApolloWrapper } from "@/lib/client"; //Importing the ApolloWrapper Provider
6const inter = Inter({ subsets: ["latin"] });
7import Header from "@/components/Header";
8
9//The rest of the code
10
11export default function RootLayout({
12 children,
13}: {
14 children: React.ReactNode;
15}) {
16 return (
17 <html lang="en">
18 <body className={inter.className} suppressHydrationWarning={true}>
19 <ApolloWrapper>
20 {/* Wrapping the React.ReactNode*/}
21 <Header />
22 {/* Header */}
23 {children}
24 </ApolloWrapper>
25 </body>
26 </html>
27 );
28}
In the AddTodo.tsx
add:
1// src/container/AddTodo.tsx
2import { useState } from "react";
3function AddTodo({ addTodo }: { addTodo: FunctionStringCallback }) {
4 const [todo, setTodo] = useState<string>("");
5 return (
6 <>
7 <div className="addTodoContainer">
8 <input
9 className="todoInputText"
10 type="text"
11 placeholder="Add new todo here..."
12 id="todoText"
13 value={todo}
14 onChange={(e) => {
15 setTodo(e.target.value);
16 }}
17 onKeyDown={(e) => {
18 if (e.code === "Enter") {
19 addTodo(todo);
20 setTodo("");
21 }
22 }}
23 />
24 <input
25 className="todoInputButton"
26 type="button"
27 value="Add Todo"
28 onClick={() => {
29 addTodo(todo);
30 setTodo("");
31 }}
32 />
33 </div>
34 </>
35 );
36}
37export 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:
1// src/coomponents/TodoItem.tsx
2interface ItemTodo {
3 todo: any;
4 editTodoItem: FunctionStringCallback;
5 deleteTodoItem: FunctionStringCallback;
6}
7function TodoItem({ todo, editTodoItem, deleteTodoItem }: ItemTodo) {
8 return (
9 <>
10 <div className="todoItem">
11 <div>{todo.attributes.todoText}</div>
12 <div>
13 <i>
14 <button className="bg-default" onClick={() => editTodoItem(todo)}>
15 Edit
16 </button>
17 </i>
18 <i>
19 <button className="bg-danger" onClick={() => deleteTodoItem(todo)}>
20 Del
21 </button>
22 </i>
23 </div>
24 </div>
25 </>
26 );
27}
28export 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.
1// src/containers/TodoList.tsx
2import TodoItem from "@/components/TodoItem";
3interface ListTodo {
4 todos: any;
5 editTodoItem: any;
6 deleteTodoItem: any;
7}
8function TodoList({ todos, editTodoItem, deleteTodoItem }: ListTodo) {
9 return (
10 <div className="todoListContainer">
11 <div className="todosText">Todos</div>
12 {todos
13 ?.sort((a: any, b: any) =>
14 b.attributes.createdAt.localeCompare(a.attributes.createdAt)
15 )
16 .map((todo: any) => {
17 return (
18 <TodoItem
19 todo={todo}
20 key={todo.id}
21 deleteTodoItem={deleteTodoItem}
22 editTodoItem={editTodoItem}
23 />
24 );
25 })}
26 </div>
27 );
28}
29export 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.
1// src/app/page.tsx
2"use client";
3import { useEffect, useState } from "react";
4import AddTodo from "@/containers/AddTodo";
5import TodoList from "@/containers/TodoList";
6export default function Home() {
7 const [todos, setTodos] = useState<[]>([]);
8 const addTodo = async (todoText: string) => {};
9 const editTodoItem = async (todo: any) => {
10 console.log("Edited");
11 };
12 const deleteTodoItem = async (todo: any) => {
13 console.log("Deleted");
14 };
15 return (
16 <div>
17 <main className="main">
18 <AddTodo addTodo={addTodo} />
19 <TodoList
20 todos={todos}
21 deleteTodoItem={deleteTodoItem}
22 editTodoItem={editTodoItem}
23 />
24 </main>
25 </div>
26 );
27}
We have arrived at the main part of this tutorial. In this section, we will handle
Let’s Start 🎉 .
Create a folder in the src
directory called query
, create a file named schema.tsand add the following lines of code to the
schema.ts` file.
1// src/query/schema.ts
2import { gql } from "@apollo/client";
3export const GETQUERY = gql`
4 {
5 todos(sort: "id:desc") {
6 data {
7 id
8 attributes {
9 todoText
10 createdAt
11 }
12 }
13 }
14 }
15`;
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:
1// src/app/page.tsx
2"use client";
3import { useEffect, useState } from "react";
4import AddTodo from "@/containers/AddTodo";
5import TodoList from "@/containers/TodoList";
6import { useQuery } from "@apollo/experimental-nextjs-app-support/ssr";
7import { GETQUERY } from "@/query/schema";
8
9export default function Home() {
10 const [todos, setTodos] = useState<[]>([]);
11 const { loading, error, data } = useQuery(GETQUERY, {
12 fetchPolicy: "no-cache",
13 }); //Fetching all todos
14 useEffect(() => {
15 setTodos(data?.todos?.data); //Storing all the todos
16 }, [data]);
17
18 //rest of the code
19}
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:
Strapi by default, sets the maximum amount of data retrieved to 10. To change this, view Strapi’s original documentation.
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
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.
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.
1// src/query/schema.ts
2import { gql } from "@apollo/client";
3
4//The rest of the code
5
6export const ADDMUT = gql`
7 mutation createTodo($todoText: String) {
8 createTodo(data: { todoText: $todoText }) {
9 data {
10 id
11 attributes {
12 todoText
13 createdAt
14 }
15 }
16 }
17 }
18`;
To add a to-do, we will import the ADDQUERY
schema from schema.ts
and the useMutation
function from apollo/client
.
1// src/app/page.tsx
2
3//The rest of the code
4
5import { useMutation } from "@apollo/client";
6import { GETQUERY, ADDMUT } from "@/query/schema";
7
8export default function Home() {
9 const [todos, setTodos] = useState<[]>([]);
10 const [createTodo] = useMutation(ADDMUT);
11 const { loading, error, data } = useQuery(GETQUERY, {
12 fetchPolicy: "no-cache",
13 }); //Fetching all todos
14 useEffect(() => {
15 console.log(data?.todos?.data);
16 setTodos(data?.todos?.data); //Storing all the todos
17 }, [data]);
18 const addTodo = async (todoText: string) => {
19 await createTodo({
20 //Creating a new todo
21 variables: {
22 todoText: todoText, //Passing the todo text
23 },
24 }).then(({ data }: any) => {
25 setTodos([...todos, data?.createTodo?.data] as any); //Adding the new todo to the list
26 });
27 };
28
29 //The rest of the code.
30}
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.
1// src/query/schema.ts
2import { gql } from "@apollo/client";
3
4//The rest of the code
5
6export const UPDATEMUT = gql`
7 mutation updateTodo($id: ID!, $todoText: String!) {
8 updateTodo(id: $id, data: { todoText: $todoText }) {
9 data {
10 id
11 attributes {
12 todoText
13 createdAt
14 }
15 }
16 }
17 }
18`;
Now, import the UPDATEMUT schema into the pages.tsx file.
1// src/app/page.tsx
2//The rest of the code
3
4import { GETQUERY, ADDMUT, UPDATEMUT } from "@/query/schema";
5export default function Home() {
6 const [todos, setTodos] = useState<[]>([]);
7 const [createTodo] = useMutation(ADDMUT);
8 const [updateTodo] = useMutation(UPDATEMUT);
9
10 //The rest of the code
11
12 const editTodoItem = async (todo: any) => {
13 const newTodoText = prompt("Enter new todo text or description:");
14 if (newTodoText != null) {
15 await updateTodo({
16 //updating the todo
17 variables: {
18 id: todo.id,
19 todoText: newTodoText,
20 },
21 }).then(({ data }: any) => {
22 const moddedTodos: any = todos.map((_todo: any) => {
23 if (_todo.id === todo.id) {
24 return data?.updateTodo?.data;
25 } else {
26 return _todo;
27 }
28 });
29 setTodos(moddedTodos);
30 });
31 }
32 };
33
34 //The rest of the code
35}
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
.
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
.
1// src/query/schema.ts
2import { gql } from "@apollo/client";
3
4//The rest of the code
5
6export const DELETEMUT = gql`
7 mutation deleteTodo($id: ID!) {
8 deleteTodo(id: $id) {
9 data {
10 id
11 attributes {
12 todoText
13 createdAt
14 }
15 }
16 }
17 }
18`;
Import the DELETEMUT
schema into pages.tsx.
1// src/app/page.tsx
2//The rest of the code
3
4import { GETQUERY, ADDMUT, UPDATEMUT, DELETEMUT } from "@/query/schema";
5export default function Home() {
6 const [todos, setTodos] = useState<[]>([]);
7 const [createTodo] = useMutation(ADDMUT);
8 const [updateTodo] = useMutation(UPDATEMUT);
9 const [deleteMUT] = useMutation(DELETEMUT);
10
11 //The rest of the code
12 const deleteTodoItem = async (todo: any) => {
13 if (confirm("Do you really want to delete this item?")) {
14 await deleteMUT({
15 //Deleting the todo
16 variables: {
17 id: todo.id,
18 },
19 }).then(({ data }: any) => {
20 const newTodos = todos.filter((_todo: any) => _todo.id !== todo.id);
21 setTodos(newTodos as any);
22 });
23 }
24 };
25 //The rest of the code
26}
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.
You can find the source code of the application here:
You can also decide to read more on Strapi and graphql using the links:
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.
Full Stack Web Developer. Loves JavaScript.