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.
What are Custom Controllers in Strapi?
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:
- Read the last five messages,
- Send a message,
- Edit a message, and
- Delete a message
Prerequisites
To follow along with this tutorial, you’ll need the following:
- Node.js—this tutorial uses Node v16.14.0
- npm—this tutorial uses npm v8.3.1
- Strapi—this tutorial uses Strapi v4.1.7
The entire source code is available in this GitHub repository.
Setting Up the Project
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-strapiIn the custom-controller-strapi directory, you’ll install both Strapi and Svelte.js projects.
Setting Up Strapi v4
In your terminal, execute the following command to create the Strapi project:
npx create-strapi-app@latest backend --quickstartThis 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:
Creating Messages Collection Type
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:
- content: Text field with Long text type
- postedBy: Text field with Short text type
- timesUpdated: Number field with number format as Integer
- uid: UID field attached to the content field
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.
Setting Up Permissions for Messages API
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.
Customizing Controllers in Strapi
There are three ways to customize the core controllers in Strapi:
- Create a custom controller
- Wrap a core action (leaves core logic in place)
- Replace a core action
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:
- You use Strapi’s Service API to query the data (
strapi.entityService.findMany) from the Message collection type. You use thesortandlimitfilters to fetch the last fiveentriesfrom the Message collection type. - You sanitize the
entriesusing Strapi’s built-insanitizeOutputmethod. - You transform the
sanitizedEntriesusing Strapi’s built-intransformResponsemethod.
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:
- You import the
nanoidpackage. - You add the
uidandtimesUpdatedproperties to the request data (ctx.request.body.data). For generating theuid, you use the nanoid npm package. - You call the core controller method (
super.create) for creating a new entry in the Message collection type. - You return the
responsethat 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 nanoidNext, 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:
- You get the
idfrom thectx.paramsby using object destructuring. - To update the
timesUpdatedproperty, you find the entry withidin the Message collection type using Strapi’s Service API (strapi.entityService.findOne) to get the existing value for thetimesUpdatedproperty. - You remove the
uidandpostedByproperties from the request body, since you don’t want the end user to update these properties. - You update the
timesUpdatedproperty by incrementing the existing value by one. - You call the core controller method (
super.update) for updating the entry in the Message collection type. - You return the
responsethat contains the details related to the updated entry.
Next, you need to connect to Strapi from your Next.js frontend application.
Setting Up Next.js Project
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@latestOn 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 devThis 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:
Installing Required npm Packages
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 axiosDon’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.
Writing an HTTP Service
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:
- You import the
axiospackage. - You define an Axios instance (
axios) and pass thebaseURLandheadersparameters. - You define the
MessagesAPIServiceobject 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.
- You export the
MessagesAPIServiceobject.
Creating UI Components
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:
- You import the
useStatehook from React. - You define the utility method (
formatDate) for formatting the dates and times. - You define the
Messageboxcomponent 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.
- You define two state variables—
isEditingandmessageText. - You define the handler function (
handleOnEdit) for theonEditevent in which you call the callback function passed to theonEditprop. - You define the handler function (
handleOnDelete) for theonDeleteevent in which you call the callback function passed to theonDeleteprop. - You render the UI for the
Messageboxcomponent in which you use the<p>tag as a content editable element to edit the message’s contents. - You export the
Messageboxcomponent.
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:
- You import the required npm packages and hooks from React.
- You define the
PublicMessagesPagefunctional component. - You define the
statevariables for thePublicMessagesPagecomponent using theuseStateReact hook:user—Stores the current user’s name.message—Stores the currently typed message.messages—Stores all the messages fetched from the Strapi API.
- You define the
fetchMessagesmethod to get messages from the API and then update the state. - You call the
fetchMessagemethod when the component is mounted by using theuseEffecthook. - You define the
handleSendMessagemethod in which you validate whether theuserandmessagestate variables are not empty. Then you call thecreatemethod from theMessageAPIServiceand pass the requireddata. Once the request is successful, you refresh the messages by calling thefetchMessagesmethod. - You define the
handleEditMessagemethod in which you validate whether themessagestate variable is not empty. Then you call theupdatemethod from theMessageAPIServiceand pass the requiredidanddata. Once the request is successful, you refresh the messages by calling thefetchMessagesmethod. - You define the
handleDeleteMessagemethod in which you call thedeletemethod from theMessageAPIServiceand pass the requiredid. Once the request is successful, you refresh the messages by calling thefetchMessagesmethod. - You return the UI for the
PublicMessagesPagecomponent. - You export the
PublicMessagesPagecomponent.
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:
- You import the
PublicMessagesPagecomponent. - You define the
Homecomponent for the localhost:3000 route in which you return thePublicMessagesPagecomponent. - You export the
Homecomponent as adefaultexport.
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:
Refreshing Messages
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:
- You import the
useIntervalhook. - You use the
useIntervalhook to call thefetchMessagesmethod every ten seconds (10000ms).
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.
Conclusion
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.