Strapi continues to be the most popular free, open-source, headless CMS, and, recently, it released v4. Built using Node.js with support for TypeScript, Strapi allows developers to perform CRUD operations using either REST or GraphQL APIs.
The best part of Strapi is that it allows users to customize its behavior, whether for the admin panel or the core business logic of your backend. You can modify its default controllers to include your own logic. For example, you might want to send an email when a new order is created.
In this tutorial, you’ll learn how to build a messaging app with Strapi on the backend and Next.js on the frontend. For this app, you’ll customize the default controllers to set up your own business logic.
Controllers stand for C in the MVC (Model-View-Controller) pattern. A controller is a class or file that contains methods to respond to the request made by the client to a route. Each route has a controller method associated with it, whose responsibility is to perform code operations and send back responses to the client.
In Strapi, whenever you create a new collection type, routes and controllers are created automatically in order to perform CRUD operations.
Depending on the complexity of your application, you might need to add your own logic to the controllers. For example, to send an email when a new order is created, you can add a piece of code to the controller associated with the /api/orders/create
route of the Order collection type and then call the original controller code. This is where Strapi shines, because it allows you to extend or replace the entire core logic for the controllers.
You’re going to customize the core controllers in Strapi by building a small but fun messaging application.
In this application, a user will be able to:
To follow along with this tutorial, you’ll need the following:
The entire source code is available in this GitHub repository.
You’ll need a master directory that holds the code for both the frontend (Next.js) and backend (Strapi).
Open up your terminal, navigate to a path of your choice, and create a project directory by running the following command:
mkdir custom-controller-strapi
In the custom-controller-strapi
directory, you’ll install both Strapi and Svelte.js projects.
In your terminal, execute the following command to create the Strapi project:
npx create-strapi-app@latest backend --quickstart
This command will create a Strapi project with quickstart settings in the backend
directory.
Once the execution completes for the above command, your Strapi project will start on port 1337 and open up localhost:1337/admin/auth/register-admin in your browser.
Set up your administrative user:
Enter your details and click the Let’s Start button. You’ll be taken to the Strapi dashboard:
Under the Plugins header in the left sidebar, click the Content-Types Builder tab. Then click Create new collection type to create a new Strapi collection:
In the modal that appears, create a new collection type with Display Name - Message and click Continue:
Next, create the following four fields for your collection type:
Click the Finish button and save your collection type by clicking the Save button:
Your collection type is set up. Next you need to configure its permissions to access it via API routes.
By default, Strapi API endpoints are not publicly accessible. For this app, you want to allow public access to it without any authentication. You need to update the permissions for the Public role.
First, click the Settings tab under the General header and then select Roles under the Users & Permissions Plugin. Click the Edit icon to the right of the Public Role:
Scroll down to find the Permissions tab and check all the allowed actions for the Message collection type. Click Save to save the updated permissions:
Next, you need to customize the controller to implement the business logic for your application.
There are three ways to customize the core controllers in Strapi:
To implement the logic for your messaging application, you need to configure three controller methods: find
, create
, and update
in the Messages collection type.
First, open message.js
file in src/api/message/controllers
and replace the existing code with the following:
1"use strict";
2
3/**
4 * message controller
5 */
6
7const { createCoreController } = require("@strapi/strapi").factories;
8
9module.exports = createCoreController("api::message.message", ({ strapi }) => ({
10 async find(ctx) {
11 // todo
12 },
13
14 async create(ctx) {
15 // todo
16 },
17
18 async update(ctx) {
19 // todo
20 },
21}));
Add the following code for the find
controller method:
1async find(ctx) {
2 // 1
3 const entries = await strapi.entityService.findMany(
4 'api::message.message',
5 {
6 sort: { createdAt: 'DESC' },
7 limit: 5,
8 }
9 );
10
11 // 2
12 const sanitizedEntries = await this.sanitizeOutput(entries, ctx);
13
14 // 3
15 return this.transformResponse(sanitizedEntries);
16},
In the above code:
strapi.entityService.findMany
) from the Message collection type. You use the sort
and limit
filters to fetch the last five entries
from the Message collection type.entries
using Strapi’s built-in sanitizeOutput
method.sanitizedEntries
using Strapi’s built-in transformResponse
method.Basically, you replace an entire core action for the find
controller method and implement your own logic.
For the create
controller method, you need to add a uid
and timesUpdated
property to the request data. Add the following code for the create
controller method:
1'use strict';
2
3// 1
4const { nanoid } = require('nanoid');
5
6...
7
8module.exports = createCoreController('api::message.message', ({ strapi }) => ({
9 ...
10
11 async create(ctx) {
12 // 2
13 ctx.request.body.data = {
14 ...ctx.request.body.data,
15 uid: nanoid(),
16 timesUpdated: 0,
17 };
18
19 // 3
20 const response = await super.create(ctx);
21
22 // 4
23 return response;
24 },
25}));
In the above code:
nanoid
package.uid
and timesUpdated
properties to the request data (ctx.request.body.data
). For generating the uid
, you use the nanoid npm package.super.create
) for creating a new entry in the Message collection type.response
that contains the details related to the newly created entry.You can install the nanoid
package by running the following command in your terminal:
npm i nanoid
Next, you need to customize the code for the update
controller method.
Whenever the update
controller method is called, you want to increment the timesUpdated
property by one. Also, you want to prevent the update of uid
and postedBy
properties, because you only want the user to update the content
property.
For this, add the following code for the update
controller method:
1async update(ctx) {
2 // 1
3 const { id } = ctx.params;
4
5 // 2
6 const entry = await strapi.entityService.findOne(
7 'api::message.message',
8 id
9 );
10
11 // 3
12 delete ctx.request.body.data.uid;
13 delete ctx.request.body.data.postedBy;
14
15 // 4
16 ctx.request.body.data = {
17 ...ctx.request.body.data,
18 timesUpdated: entry.timesUpdated + 1,
19 };
20
21 // 5
22 const response = await super.update(ctx);
23
24 // 6
25 return response;
26},
In the above code:
id
from the ctx.params
by using object destructuring.timesUpdated
property, you find the entry with id
in the Message collection type using Strapi’s Service API (strapi.entityService.findOne
) to get the existing value for the timesUpdated
property.uid
and postedBy
properties from the request body, since you don’t want the end user to update these properties.timesUpdated
property by incrementing the existing value by one.super.update
) for updating the entry in the Message collection type.response
that contains the details related to the updated entry.Next, you need to connect to Strapi from your Next.js frontend application.
You need to build the Next.js frontend application and integrate it with the Strapi backend.
First, in the custom-controllers-strapi
directory, run the following command to create a Next.js project:
npx create-next-app@latest
On the terminal, when you are asked about the project’s name, set it to frontend
. It will install the npm dependencies.
After the installation is complete, navigate into the frontend
directory and start the Next.js development server by running the following commands in your terminal:
cd frontend
npm run dev
This will start the development server on port 3000 and take you to localhost:3000. The first view of the Next.js website will look like this:
To make HTTP calls to the backend server, you can use the Axios npm package. To install it, run the following command in your terminal:
npm i axios
Don’t worry about styling the app right now—you’re concentrating on core functionality. You can learn to set up and customize Bootstrap in Next.js later in case you’d like to use Bootstrap as your UI framework.
You need an HTTP service to connect with the Strapi API and perform CRUD operations.
First, create a services
directory in the frontend
directory. In the services
directory, create a MessagesApi.js
file and add the following code to it:
1// 1
2import { Axios } from "axios";
3
4// 2
5const axios = new Axios({
6 baseURL: "http://localhost:1337/api",
7 headers: {
8 "Content-Type": "application/json",
9 },
10});
11
12// 3
13const MessagesAPIService = {
14 find: async () => {
15 const response = await axios.get("/messages");
16 return JSON.parse(response.data).data;
17 },
18
19 create: async ({ data }) => {
20 const response = await axios.post(
21 "/messages",
22 JSON.stringify({ data: data })
23 );
24 return JSON.parse(response.data).data;
25 },
26
27 update: async ({ id, data }) => {
28 const response = await axios.put(
29 `/messages/${id}`,
30 JSON.stringify({ data: data })
31 );
32 return JSON.parse(response.data).data;
33 },
34
35 delete: async ({ id }) => {
36 const response = await axios.delete(`/messages/${id}`);
37 return JSON.parse(response.data).data;
38 },
39};
40
41// 4
42export { MessagesAPIService };
In the above code:
axios
package.axios
) and pass the baseURL
and headers
parameters.MessagesAPIService
object and define the methods for the following:find
: this method returns a list of the last five messages.create
: this method is used to create a new message.update
: this method is used to edit an existing message.delete
: this method is used to delete a message.MessagesAPIService
object.You’re going to create two components, one for rendering search results and another for rendering the search suggestions.
Create a components
directory in the frontend
directory. In the components
directory, create a Messagebox.js
file and add the following code to it:
1// 1
2import { useState } from "react";
3
4// 2
5const formatDate = (value) => {
6 if (!value) {
7 return "";
8 }
9 return new Date(value).toLocaleTimeString();
10};
11
12// 3
13function Messagebox({ message, onEdit, onDelete }) {
14 // 4
15 const [isEditing, setIsEditing] = useState(false);
16 const [messageText, setMessageText] = useState(message.attributes.content);
17
18 // 5
19 const handleOnEdit = async (e) => {
20 e.preventDefault();
21 await onEdit({ id: message.id, message: messageText });
22 setIsEditing(false);
23 };
24
25 // 6
26 const handleOnDelete = async (e) => {
27 e.preventDefault();
28 await onDelete({ id: message.id });
29 };
30
31 // 7
32 return (
33 <div>
34 <div>
35 <p>{formatDate(message.attributes.createdAt)}</p>
36 <b>{message.attributes.postedBy}</b>
37 <p
38 contentEditable
39 onFocus={() => setIsEditing(true)}
40 onInput={(e) => setMessageText(e.target.innerText)}
41 >
42 {message.attributes.content}
43 </p>
44 </div>
45 <div>
46 {message.attributes.timesUpdated > 0 && (
47 <p>Edited {message.attributes.timesUpdated} times</p>
48 )}
49 {isEditing && (
50 <>
51 <button onClick={handleOnEdit}>Save</button>
52 <button onClick={() => setIsEditing(false)}>Cancel</button>
53 </>
54 )}
55 <button onClick={handleOnDelete}>Delete</button>
56 </div>
57 </div>
58 );
59}
60
61// 8
62export default Messagebox;
In the above code:
useState
hook from React.formatDate
) for formatting the dates and times.Messagebox
component that is used to render the data related to an individual message. This component takes in three props:message
—A message object returned from the API.onEdit
—Callback function to run when a message is edited.onDelete
—Callback function to run when a message is deleted.isEditing
and messageText
.handleOnEdit
) for the onEdit
event in which you call the callback function passed to the onEdit
prop.handleOnDelete
) for the onDelete
event in which you call the callback function passed to the onDelete
prop.Messagebox
component in which you use the <p>
tag as a content editable element to edit the message’s contents.Messagebox
component.Next, create a PublicMessagesPage.js
file in the components
directory and add the following code to it:
1// 1
2import React, { useState, useEffect } from "react";
3import Messagebox from "./Messagebox";
4import { MessagesAPIService } from "../services/MessagesApi";
5
6// 2
7function PublicMessagesPage() {
8 // 3
9 const [user, setUser] = useState("");
10 const [message, setMessage] = useState("");
11 const [messages, setMessages] = useState([]);
12
13 // 4
14 const fetchMessages = async () => {
15 const messages = await MessagesAPIService.find();
16 setMessages(messages);
17 };
18
19 // 5
20 useEffect(() => {
21 fetchMessages();
22 }, []);
23
24 // 6
25 const handleSendMessage = async (e) => {
26 e.preventDefault();
27
28 if (!user) {
29 alert("Please add your username");
30 return;
31 }
32
33 if (!message) {
34 alert("Please add a message");
35 return;
36 }
37
38 await MessagesAPIService.create({
39 data: {
40 postedBy: user,
41 content: message,
42 },
43 });
44
45 await fetchMessages();
46 setMessage("");
47 };
48
49 // 7
50 const handleEditMessage = async ({ id, message }) => {
51 if (!message) {
52 alert("Please add a message");
53 return;
54 }
55
56 await MessagesAPIService.update({
57 id: id,
58 data: {
59 content: message,
60 },
61 });
62
63 await fetchMessages();
64 };
65
66 // 8
67 const handleDeleteMessage = async ({ id }) => {
68 if (confirm("Are you sure you want to delete this message?")) {
69 await MessagesAPIService.delete({ id });
70 await fetchMessages();
71 }
72 };
73
74 // 9
75 return (
76 <div>
77 <div>
78 <h1>Random Talk</h1>
79 <p>Post your random thoughts that vanish</p>
80 </div>
81
82 <div>
83 <form onSubmit={(e) => handleSendMessage(e)}>
84 <input
85 type="text"
86 value={user}
87 onChange={(e) => setUser(e.target.value)}
88 required
89 />
90 <div className="d-flex align-items-center overflow-hidden">
91 <input
92 type="text"
93 value={message}
94 onChange={(e) => setMessage(e.target.value)}
95 required
96 />
97 <button onClick={(e) => handleSendMessage(e)}>Send</button>
98 </div>
99 </form>
100 </div>
101
102 <div>
103 {messages.map((message) => (
104 <Messagebox
105 key={message.attributes.uid}
106 message={message}
107 onEdit={handleEditMessage}
108 onDelete={handleDeleteMessage}
109 />
110 ))}
111 </div>
112 </div>
113 );
114}
115
116// 10
117export default PublicMessagesPage;
In the above code:
PublicMessagesPage
functional component.state
variables for the PublicMessagesPage
component using the useState
React hook:user
—Stores the current user’s name.message
—Stores the currently typed message.messages
—Stores all the messages fetched from the Strapi API.fetchMessages
method to get messages from the API and then update the state.fetchMessage
method when the component is mounted by using the useEffect
hook.handleSendMessage
method in which you validate whether the user
and message
state variables are not empty. Then you call the create
method from the MessageAPIService
and pass the required data
. Once the request is successful, you refresh the messages by calling the fetchMessages
method.handleEditMessage
method in which you validate whether the message
state variable is not empty. Then you call the update
method from the MessageAPIService
and pass the required id
and data
. Once the request is successful, you refresh the messages by calling the fetchMessages
method.handleDeleteMessage
method in which you call the delete
method from the MessageAPIService
and pass the required id
. Once the request is successful, you refresh the messages by calling the fetchMessages
method.PublicMessagesPage
component.PublicMessagesPage
component.Next, in the pages
directory, replace the existing code by adding the following code to the index.js
file:
1// 1
2import PublicMessagesPage from "../components/PublicMessagesPage";
3
4// 2
5function Home() {
6 return <PublicMessagesPage />;
7}
8
9// 3
10export default Home;
In the above code:
PublicMessagesPage
component.Home
component for the localhost:3000 route in which you return the PublicMessagesPage
component.Home
component as a default
export.Finally, save your progress and visit localhost:3000 to test your messaging app by performing different CRUD operations:
Send a message:
Edit a message:
Delete a message:
Currently, if someone adds, edits, or updates messages, you need to refresh the whole page to see that. To fetch new messages, you can poll the localhost:1337/api/messages endpoint every few seconds or use WebSockets. This tutorial will use the former approach.
To implement polling, create a utils
directory in the frontend
directory. In the utils
directory, create a hooks.js
file and add the following code to it:
1import { useEffect, useRef } from "react";
2
3function useInterval(callback, delay) {
4 const savedCallback = useRef();
5
6 // Remember the latest callback.
7 useEffect(() => {
8 savedCallback.current = callback;
9 }, [callback]);
10
11 // Set up the interval.
12 useEffect(() => {
13 function tick() {
14 savedCallback.current();
15 }
16 if (delay !== null) {
17 let id = setInterval(tick, delay);
18 return () => clearInterval(id);
19 }
20 }, [delay]);
21}
22
23export { useInterval };
In the above code, you have defined a custom hook, useInterval
, that allows you to call the callback
after every delay
. This implementation is taken from software engineer Dan Abramov’s blog.
Next, update the PublicMessagesPage.js
file by adding the following code to it:
1...
2
3// 1
4import { useInterval } from "../utils/hooks";
5
6function PublicMessagesPage() {
7 ...
8
9 // 2
10 useInterval(() => {
11 fetchMessages();
12 }, 10000);
13
14 ...
15}
In the above code:
useInterval
hook.useInterval
hook to call the fetchMessages
method every ten seconds (10000
ms).To test the auto-refresh of messages, open localhost:3000 in at least two browser tabs or windows and try sending messages by setting different usernames. Here’s how the application works now:
That’s it. You have successfully implemented a messaging application using Strapi and Next.js.
In this tutorial, you learned to create a messaging application using Strapi for building the backend and Next.js for building the frontend UI. You also learned to customize the controllers in Strapi to implement your own business logic.
To check your work on this tutorial, go to this GitHub repository.