Using Custom Controllers to Power a Next.js App
Tags: nextjs, strapi, javascript
Published: 2022-05-11 08:50:35

[Strapi](https://strapi.io/) continues to be [the most popular free, open-source, headless CMS](https://webtechsurvey.com/technology/strapi), 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](https://nextjs.org/) 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](https://docs.strapi.io/developer-docs/latest/development/backend-customization/controllers.html#implementation) 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](https://nodejs.org/en/download/)—this tutorial uses Node v16.14.0
- [npm](https://npmjs.org)—this tutorial uses npm v8.3.1
- [Strapi](https://strapi.io/getting-started)—this tutorial uses Strapi v4.1.7

[The entire source code is available in this GitHub repository](https://github.com/ravgeetdhillon/custom-controllers-strapi).

## 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:

```bash
mkdir custom-controller-strapi
```

In 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:

```bash
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](http://localhost:1337/admin/auth/register-admin) in your browser. 

Set up your **administrative user**:

![Welcome screen](https://i.imgur.com/GyiS7IF.png)

Enter your details and click the **Let’s Start** button. You’ll be taken to the Strapi dashboard:

![Strapi dashboard](https://i.imgur.com/bOGwqcN.png)

## 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:

![Create new collection type](https://i.imgur.com/cwnLjMn.png)

In the modal that appears, create a new collection type with _Display Name_ - **Message** and click **Continue**:

![Collection configurations](https://i.imgur.com/22b5z60.png)

Next, create the following four fields for your collection type:

1. **content**: _Text_ field with _Long text_ type
2. **postedBy**: _Text_ field with _Short text_ type
3. **timesUpdated**: _Number_ field with number format as _Integer_
4. **uid**: _UID_ field attached to the **content** field

Click the **Finish** button and save your collection type by clicking the **Save** button:

![Save collection type](https://i.imgur.com/Mvli8pW.png)

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:

![Edit Roles](https://i.imgur.com/K2Px2vS.png)

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:

![Save permissions](https://i.imgur.com/ya5Vals.png)

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:

```js
"use strict";

/**
 *  message controller
 */

const { createCoreController } = require("@strapi/strapi").factories;

module.exports = createCoreController("api::message.message", ({ strapi }) => ({
  async find(ctx) {
    // todo
  },

  async create(ctx) {
    // todo
  },

  async update(ctx) {
    // todo
  },
}));
```

Add the following code for the `find` controller method:

```js
async find(ctx) {
  // 1
  const entries = await strapi.entityService.findMany(
    'api::message.message',
    {
      sort: { createdAt: 'DESC' },
      limit: 5,
    }
  );

  // 2
  const sanitizedEntries = await this.sanitizeOutput(entries, ctx);

  // 3
  return this.transformResponse(sanitizedEntries);
},
```

In the above code:

1. You use [Strapi’s Service API](https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/entity-service/crud.html#entity-service-api-crud-operations) to query the data (`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.
2. You sanitize the `entries` using Strapi’s built-in `sanitizeOutput` method.
3. You transform the `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:

```js
'use strict';

// 1
const { nanoid } = require('nanoid');

...

module.exports = createCoreController('api::message.message', ({ strapi }) => ({
  ...

  async create(ctx) {
    // 2
    ctx.request.body.data = {
      ...ctx.request.body.data,
      uid: nanoid(),
      timesUpdated: 0,
    };

    // 3
    const response = await super.create(ctx);

    // 4
    return response;
  },
}));
```

In the above code:

1. You import the `nanoid` package.
2. You add the `uid` and `timesUpdated` properties to the request data (`ctx.request.body.data`). For generating the `uid`, you use the [nanoid](https://www.npmjs.com/package/nanoid) npm package.
3. You call the core controller method (`super.create`) for creating a new entry in the Message collection type.
4. You return the `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:

```bash
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:

```js
async update(ctx) {
  // 1
  const { id } = ctx.params;

  // 2
  const entry = await strapi.entityService.findOne(
    'api::message.message',
    id
  );

  // 3
  delete ctx.request.body.data.uid;
  delete ctx.request.body.data.postedBy;

  // 4
  ctx.request.body.data = {
    ...ctx.request.body.data,
    timesUpdated: entry.timesUpdated + 1,
  };

  // 5
  const response = await super.update(ctx);

  // 6
  return response;
},
```

In the above code:

1. You get the `id` from the `ctx.params` by using object destructuring.
2. To update the `timesUpdated` property, you find the entry with `id` in the Message collection type using Strapi’s [Service API](https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/entity-service/crud.html#entity-service-api-crud-operations) (`strapi.entityService.findOne`) to get the existing value for the `timesUpdated` property.
3. You remove the `uid` and `postedBy` properties from the request body, since you don’t want the end user to update these properties.
4. You update the `timesUpdated` property by incrementing the existing value by one.
5. You call the core controller method (`super.update`) for updating the entry in the Message collection type.
6. You return the `response` that 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:

```bash
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:

```bash
cd frontend
npm run dev
```

This will start the development server on port _3000_ and take you to [localhost:3000](http://localhost:3000/). The first view of the Next.js website will look like this:

![Next.js welcome screen](https://i.imgur.com/lFo0MtH.png)

## Installing Required npm Packages

To make HTTP calls to the backend server, you can use the [Axios](https://www.npmjs.com/package/axios) npm package. To install it, run the following command in your terminal:

```bash
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](https://www.ravsam.in/blog/setup-and-customize-bootstrap-in-nextjs/) 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:

```js
// 1
import { Axios } from "axios";

// 2
const axios = new Axios({
  baseURL: "http://localhost:1337/api",
  headers: {
    "Content-Type": "application/json",
  },
});

// 3
const MessagesAPIService = {
  find: async () => {
    const response = await axios.get("/messages");
    return JSON.parse(response.data).data;
  },

  create: async ({ data }) => {
    const response = await axios.post(
      "/messages",
      JSON.stringify({ data: data })
    );
    return JSON.parse(response.data).data;
  },

  update: async ({ id, data }) => {
    const response = await axios.put(
      `/messages/${id}`,
      JSON.stringify({ data: data })
    );
    return JSON.parse(response.data).data;
  },

  delete: async ({ id }) => {
    const response = await axios.delete(`/messages/${id}`);
    return JSON.parse(response.data).data;
  },
};

// 4
export { MessagesAPIService };
```

In the above code:

1. You import the `axios` package.
2. You define an Axios instance (`axios`) and pass the `baseURL` and `headers` parameters.
3. You define the `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.
4. You export the `MessagesAPIService` object.

## 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:

```js
// 1
import { useState } from "react";

// 2
const formatDate = (value) => {
  if (!value) {
    return "";
  }
  return new Date(value).toLocaleTimeString();
};

// 3
function Messagebox({ message, onEdit, onDelete }) {
  // 4
  const [isEditing, setIsEditing] = useState(false);
  const [messageText, setMessageText] = useState(message.attributes.content);

  // 5
  const handleOnEdit = async (e) => {
    e.preventDefault();
    await onEdit({ id: message.id, message: messageText });
    setIsEditing(false);
  };

  // 6
  const handleOnDelete = async (e) => {
    e.preventDefault();
    await onDelete({ id: message.id });
  };

  // 7
  return (
    <div>
      <div>
        <p>{formatDate(message.attributes.createdAt)}</p>
        <b>{message.attributes.postedBy}</b>
        <p
          contentEditable
          onFocus={() => setIsEditing(true)}
          onInput={(e) => setMessageText(e.target.innerText)}
        >
          {message.attributes.content}
        </p>
      </div>
      <div>
        {message.attributes.timesUpdated > 0 && (
          <p>Edited {message.attributes.timesUpdated} times</p>
        )}
        {isEditing && (
          <>
            <button onClick={handleOnEdit}>Save</button>
            <button onClick={() => setIsEditing(false)}>Cancel</button>
          </>
        )}
        <button onClick={handleOnDelete}>Delete</button>
      </div>
    </div>
  );
}

// 8
export default Messagebox;
```

In the above code:

1. You import the `useState` hook from React.
2. You define the utility method (`formatDate`) for formatting the dates and times.
3. You define the `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.
4. You define two state variables—`isEditing` and `messageText`.
5. You define the handler function (`handleOnEdit`) for the `onEdit` event in which you call the callback function passed to the `onEdit` prop.
6. You define the handler function (`handleOnDelete`) for the `onDelete` event in which you call the callback function passed to the `onDelete` prop.
7. You render the UI for the `Messagebox` component in which you use the `<p>` tag as a content editable element to edit the message’s contents.
8. You export the `Messagebox` component.

Next, create a `PublicMessagesPage.js` file in the `components` directory and add the following code to it:

```js
// 1
import React, { useState, useEffect } from "react";
import Messagebox from "./Messagebox";
import { MessagesAPIService } from "../services/MessagesApi";

// 2
function PublicMessagesPage() {
  // 3
  const [user, setUser] = useState("");
  const [message, setMessage] = useState("");
  const [messages, setMessages] = useState([]);

  // 4
  const fetchMessages = async () => {
    const messages = await MessagesAPIService.find();
    setMessages(messages);
  };

  // 5
  useEffect(() => {
    fetchMessages();
  }, []);

  // 6
  const handleSendMessage = async (e) => {
    e.preventDefault();

    if (!user) {
      alert("Please add your username");
      return;
    }

    if (!message) {
      alert("Please add a message");
      return;
    }

    await MessagesAPIService.create({
      data: {
        postedBy: user,
        content: message,
      },
    });

    await fetchMessages();
    setMessage("");
  };

  // 7
  const handleEditMessage = async ({ id, message }) => {
    if (!message) {
      alert("Please add a message");
      return;
    }

    await MessagesAPIService.update({
      id: id,
      data: {
        content: message,
      },
    });

    await fetchMessages();
  };

  // 8
  const handleDeleteMessage = async ({ id }) => {
    if (confirm("Are you sure you want to delete this message?")) {
      await MessagesAPIService.delete({ id });
      await fetchMessages();
    }
  };

  // 9
  return (
    <div>
      <div>
        <h1>Random Talk</h1>
        <p>Post your random thoughts that vanish</p>
      </div>

      <div>
        <form onSubmit={(e) => handleSendMessage(e)}>
          <input
            type="text"
            value={user}
            onChange={(e) => setUser(e.target.value)}
            required
          />
          <div className="d-flex align-items-center overflow-hidden">
            <input
              type="text"
              value={message}
              onChange={(e) => setMessage(e.target.value)}
              required
            />
            <button onClick={(e) => handleSendMessage(e)}>Send</button>
          </div>
        </form>
      </div>

      <div>
        {messages.map((message) => (
          <Messagebox
            key={message.attributes.uid}
            message={message}
            onEdit={handleEditMessage}
            onDelete={handleDeleteMessage}
          />
        ))}
      </div>
    </div>
  );
}

// 10
export default PublicMessagesPage;
```

In the above code:

1. You import the required npm packages and hooks from React.
2. You define the `PublicMessagesPage` functional component.
3. You define the `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.
4. You define the `fetchMessages` method to get messages from the API and then update the state.
5. You call the `fetchMessage` method when the component is mounted by using the `useEffect` hook.
6. You define the `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.
7. You define the `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.
8. You define the `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.
9. You return the UI for the `PublicMessagesPage` component.
10. You export the `PublicMessagesPage` component.

Next, in the `pages` directory, replace the existing code by adding the following code to the `index.js` file:

```js
// 1
import PublicMessagesPage from "../components/PublicMessagesPage";

// 2
function Home() {
  return <PublicMessagesPage />;
}

// 3
export default Home;
```

In the above code:

1. You import the `PublicMessagesPage` component.
2. You define the `Home` component for the [localhost:3000](http://localhost:3000) route in which you return the `PublicMessagesPage` component.
3. You export the `Home` component as a `default` export.

Finally, save your progress and visit [localhost:3000](http://localhost:3000) to test your messaging app by performing different CRUD operations:

- Send a message:

  ![Send a message](https://i.imgur.com/t2fuU7D.gif)

- Edit a message:

  ![Edit a message](https://i.imgur.com/QVYAIn0.gif)

- Delete a message:

  ![Delete a message](https://i.imgur.com/j6BccCX.gif)

## 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](http://localhost:1337/api/messages) endpoint every few seconds or use [WebSockets](https://strapi.io/blog/how-to-build-a-real-time-chat-forum-using-strapi-socket-io-react-and-mongo-db). 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:

```js
import { useEffect, useRef } from "react";

function useInterval(callback, delay) {
  const savedCallback = useRef();

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

export { useInterval };
```

In the above code, you have defined a custom hook, `useInterval`, that allows you to call the `callback` after every `delay`. This [implementation](https://overreacted.io/making-setinterval-declarative-with-react-hooks/) is taken from software engineer Dan Abramov’s blog.

Next, update the `PublicMessagesPage.js` file by adding the following code to it:

```js
...

// 1
import { useInterval } from "../utils/hooks";

function PublicMessagesPage() {
  ...

  // 2
  useInterval(() => {
    fetchMessages();
  }, 10000);

  ...
}
```

In the above code:

1. You import the `useInterval` hook.
2. You use the `useInterval` hook to call the `fetchMessages` method every ten seconds (`10000` ms).

To test the auto-refresh of messages, open [localhost:3000](http://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:

![Application auto-refresh](https://i.imgur.com/i8cwFdX.gif)

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](https://github.com/ravgeetdhillon/custom-controllers-strapi).