Integrating Strapi with ChatGPT and Next.js
Tags: tutorial, openai, cms, nextjs
Published: 2024-04-12 08:39:56

## Introduction
In this tutorial, we will learn how to use Strapi, [ChatGPT](https://chat.openai.com/), and [Next.js](https://nextjs.org/) to build an app that displays recipes using AI.

This tutorial aims to show how to combine these different technologies and how integrating AI can enrich the user experience of our apps.

For example, in a recipe-sharing app, a content management system such as Strapi would be in charge of structured storage and management of the data about each recipe. By integrating AI, we can elevate the functionality with additional insights, suggestions, or personalisation according to the particular user.



## Prerequisites
To follow this tutorial, we will need the following:

- An [OpenAI](https://platform.openai.com/) (ChatGPT) account and API key.
- Package manager (yarn or npm).
- [Node.js](https://nodejs.org/en/download) (v16 or v18).
- Code editor (Visual Studio code, Sublime).
- Basic knowledge of Javascript.

## Overview of Strapi
Strapi or Strapi CMS is an open-source headless content management system that allows us to create APIs quickly. The headless architecture separates the content management and content delivery layers. This feature will enable us to consume the content via API and use whatever frontend technology (React.Js, Vue, etc.). Strapi CMS also provides plugins or Strapi plugins that can extend the functionality and add custom features to the CMS. The role of Strapi in this project will be to serve as the backend, which is responsible for storing and serving recipes to the frontend. So, without further ado, let's dive into the code.

## Setting up Strapi
Let's create a folder that will contain the source code for our Strapi API. First, we open a terminal and navigate to any directory of our choice, and run the commands below:

```
mkdir strapi-tutorial
cd strapi-tutorial
```

This folder will contain both our frontend and backend codes. Now, let's create our Strapi API with the command below:

```
npx create-strapi-app@latest strapi-recipe-api
```

Once this is complete and it finishes installing the dependencies, we should receive a confirmation like the one below!

![001-strapi-installation.png](https://api-prod.strapi.io/uploads/001_strapi_installation_bf23af03d5.png)

It should automatically direct us to the Strapi dashboard as it starts our project. We can create our administrator here, so we must fill out the form and click the **"Let's start"** button.

We should now see the dashboard which looks like this:

![002-strapi-welcome-on-board.png](https://api-prod.strapi.io/uploads/002_strapi_welcome_on_board_5dcfb7dc89.png)

## Create Collection Type
We want to create the collection type, which would be the recipe and its various keys and values, such as ingredients and instructions.

Click **Content-Type Builder** from the left side panel, and then on the left, under **Collection Types**, we should see **"Create new collection type"**.  Click on that and type **"Recipe"** into the display name.

![003-create-collect-type.png](https://api-prod.strapi.io/uploads/003_create_collect_type_e62465fe28.png)

Click continue, and from here, click text as the data type like this:

![004-select-field-for-collection-type.png](https://api-prod.strapi.io/uploads/004_select_field_for_collection_type_19dc95ab6e.png)

Then enter the title and make sure **short text** is selected.

![005-add-new-text-field.png](https://api-prod.strapi.io/uploads/005_add_new_text_field_fd7e74ce17.png)

Now we need to add the rest of the fields, so click on "**Add another field**," add text, and enter a description, but this time select the long text. The rest of the fields to add are like so:

- ingredients - long text
- instructions - long text

After adding instructions, we click "Finish." We should now see our newly created collection: **Recipe** and its different fields. Click **save** at the top right. This action will restart the server. Once done, we can play around with our collection and add some recipes.

From the left side panel, now select **Content Manager** under **Collection Types**, select **Recipe**, and then select **"create new entry"** on the top right.

We can ask ChatGPT to generate the inputs for us with the following prompt: "Give me a recipe idea with a title, description, ingredients, and instructions" and fill in the various fields below:


![006-create-an-entry.png](https://api-prod.strapi.io/uploads/006_create_an_entry_58fbf01658.png)

Now go ahead and click **save** in the top right. This action should prompt a success message, and now, if we click **Recipe** on the left, we should see our latest recipe entry in the table.

Tick the entry, and then select **Publish**:

![007-recipe-collection-type-with-entries.png](https://api-prod.strapi.io/uploads/007_recipe_collection_type_with_entries_5158e4b615.png)

By default, Strapi requires authentication to query our API and receive information, but that is outside the scope of this tutorial. Instead, we will make our API publicly accessible. We can find more about authentication and REST API in this [blog post](https://strapi.io/blog/guide-on-authenticating-requests-with-the-rest-api).

From the left sidebar, click on **Settings**. Again,  on the left panel under **USERS & PERMISSIONS PLUGIN**, click on **Roles**, then click on **Public** from the table on the right. Now scroll down, click on **Recipe**, and tick **Select all** to allow the user to access information without authentication.

![008-allow-public-access.png](https://api-prod.strapi.io/uploads/008_allow_public_access_b324303899.png)

Now if we copy and paste the following into our browser we should be able to fetch the data from our API.

```
http://localhost:1337/api/recipes
```

## Integrating ChatGPT with Strapi

So far we've learned what Strapi is and what it does, we've got the API up and running and added some data, now let's see how we can integrate ChatGPT in our project.

First navigate to the terminal and run the below command in the root directory:

```
yarn strapi generate
```

This will begin the process of generating our own custom API. Choose the API option, give it the name `recipe-gpt`, and select **"no"** when it asks us if this is for a plugin.

Inside the `src` directory, If we check the `api` directory in our code editor, we should see the newly created APIs for `recipe` and `recipe-gpt` with their routes, controllers, and services.

Let's check it works by uncommenting the code in each file, restarting the project in the terminal, and navigating to the admin dashboard. Now, once again, click ***Settings > Soles > Public***, then scroll down to **Select all** on the `recipe-gpt` API to make the permissions public, and click **save** in the top right.

![0010-welcome-to-nextjs.png](https://api-prod.strapi.io/uploads/0010_welcome_to_nextjs_f097d6c715.png)

Now if we enter the following into our browser and click enter, we should get an **"ok"** message.

```
http://localhost:1337/api/recipe-gpt
```

Okay, now we've confirmed the API endpoint is working, let's connect it to OpenAI first, install the OpenAI package, navigate to the route directory, and run the command below in our terminal

```
yarn add openai
```

Then in the `.env` file add the following environment variable:

```
OPENAI=< OpenAI api key here>
```

Now under the `recipe-gpt` directory change the code in the routes directory to the following:

```
module.exports = {
  routes: [
    {
      method: "POST",
      path: "/recipe-gpt/exampleAction",
      handler: "recipe-gpt.exampleAction",
      config: {
        policies: [],
        middlewares: [],
      },
    },
  ],
};
```

Change the code in the `controller` directory to the following:

```
"use strict";

module.exports = {
  exampleAction: async (ctx) => {
    try {
      const response = await strapi
        .service("api::recipe-gpt.recipe-gpt")
        .recipeService(ctx);

      ctx.body = { data: response };
    } catch (err) {
      console.log(err.message);
      throw new Error(err.message);
    }
  },
};
```

And the code in the `services` directory to the following:

```
"use strict";
const { OpenAI } = require("openai");
const openai = new OpenAI({
  apiKey: process.env.OPENAI,
});

/**
 * recipe-gpt service
 */

module.exports = ({ strapi }) => ({
  recipeService: async (ctx) => {
    try {
      const input = ctx.request.body.data?.input;
      const completion = await openai.chat.completions.create({
        messages: [{ role: "user", content: input }],
        model: "gpt-3.5-turbo",
      });

      const answer = completion.choices[0].message.content;

      return {
        message: answer,
      };
    } catch (err) {
      ctx.body = err;
    }
  },
});
```

Now we can make a post request which contains input (which will be from our frontend) and return answers from chatGPT.

We can check the connection to our post route by pasting the below code in our terminal:

```
curl -X POST \
  http://localhost:1337/api/recipe-gpt/exampleAction \
  -H 'Content-Type: application/json' \
  -d '{
    "data": {
        "input": "Give me some interesting nutritional information"
    }
}'

```

## Implement Front-end Components with Next.Js

So we now have the API set up to deliver content (recipes) and a custom API set up to provide information from ChatGPT on any input we might send to it from the frontend. Let's move on to the next part (whoops, no pun intended).

Next.js is a React framework that simplifies the development of complex and high-performance web applications. It offers many built-in features, such as server-side rendering, automatic code splitting, automatic image optimization, and API routes. Next.Js streamlines the development process and ensures fast and responsive user experiences.

Let's create our frontend directory. Navigate to the main folder, `strapi-tutorial`, and enter the following command in our terminal.

```
npx create-next-app recipe-frontend
```

Navigate into this newly created directory and run the below 

```
yarn dev
```

This should start and run the project in `http://localhost:3000`, when accessed through the web browser, we should be able to see the image below:

![009-custom-api.png](https://api-prod.strapi.io/uploads/009_custom_api_8aa23df42a.png)

Open up the project in our code editor and look under the `pages` directory and click the `index.js` file. This file is the entry point to the application and where the code for this welcome page exists. Delete everything and paste the below code into this file.

```
import Head from 'next/head';
import styles from '../styles/Home.module.css';

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Welcome to <a href="https://nextjs.org">Strapi Recipe!</a>
        </h1>
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by Strapi
        </a>
      </footer>
    </div>
  );
}
```

Save the file. We should notice the difference in our web browser as the code automatically reloads.

Next, we will want to create some re-usable components to display the recipes for our users. To speed things up, let's use a component library. Navigate to the terminal, and run the command below

```
yarn add antd
```
First, let's add a card component. Create a directory `components` outside of the `pages` directory and then create a file called `RecipeCard.jsx`. Paste the following code inside the new file:

```
import { Card } from 'antd';
import Image from 'next/image';

export default function RecipeCard({ title, bordered, content }) {
  return (
    <div style={{ cursor: 'pointer', width: '400px' }}>
      <Card
        title={title}
        bordered={bordered}
        cover={
          <Image
            alt="example"
            unoptimized
            width={400}
            height={200}
            src={'https://placehold.co/400x200'}
          />
        }
      >
        {content.length > 200 ? content.slice(0, 200) + '...' : content}
      </Card>
    </div>
  );
}
```

By default Next.Js reccomends us to use the `images.domains` to be able to load external images with `next/image`, so just add the following to our `next.config.js` file

```
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  images: {
    domains: ['placehold.co'],
  },
};

module.exports = nextConfig;
```

Let's create a way to view the details of our recipes using a modal from the component library, create a file in `components` directory called `RecipeModal.jsx` and paste in the below code

```
import { Modal } from 'antd';

export default function RecipeModal({
  title,
  open,
  setOpen,
  width = 1000,
  description,
  ingredients,
  instructions,
}) {
  return (
    <div>
      <Modal
        title={title}
        centered
        open={open}
        onOk={() => setOpen(false)}
        onCancel={() => setOpen(false)}
        width={width}
      >
        <h3>Description</h3>
        <p>{description}</p>
        <h3>Ingredients</h3>
        <p>{ingredients}</p>
        <h3>Instructions</h3>
        <p>{instructions}</p>
      </Modal>
    </div>
  );
}
```

Now, we have our components for displaying the recipes we can move on to fetching our content from Strapi and working out the logic to connect the data and the UI together.

## Fetching and displaying recipe data

So, now that we have Strapi ready with our API and we've set up the frontend Next.Js app and built some components to host our data, we're finally ready to make the connection. We will want to create some reusable piece of logic that encapsulates the fetching of our data and the storing in state; this is a perfect use for a custom hook.

First, let's create the fetch function for our API, so create a `utils` folder in the root directory. Inside this new folder, create a file called `api.js`, and paste the below code there.


```
const baseUrl = 'http://localhost:1337';

export async function fetchRecipes() {
  try {
    const res = await fetch(`${baseUrl}/api/recipes`);
    return await res.json();
  } catch (e) {
    console.error('Error fetching data:', error);
    throw error;
  }
}
```

In the root directory create a `hooks` folder, and inside that, create a file named `useGetRecipe.js` and add the  following code:

```
import { useEffect, useState } from 'react';
import { fetchRecipes } from '../utils/api';

export function useGetRecipe() {
  const [recipes, setRecipes] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchData() {
      try {
        const { data } = await fetchRecipes();
        console.log('data - ', data);
        setRecipes(data);
      } catch (e) {
        setError(e);
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, []);

  return { recipes, loading, error };
}
```

This hook encapsulates all of the logic to call our API, set the loading state while we wait for data, set the data in the state, and save any errors we might get. We can easily re-use this hook without re-writing this logic anywhere else.

Import it into `index.js` and use it to display our `RecipeCard` component. The `index.js` file should now look like the code below:


```
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useGetRecipe } from '../hooks/useGetRecipe';
import dynamic from 'next/dynamic';
const RecipeCard = dynamic(() => import('../components/RecipeCard'), {
  ssr: false,
});

export default function Home() {
  const { recipes, loading, error } = useGetRecipe();

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <div>
          <h1 className={styles.title}>
            Welcome to <a href="https://nextjs.org">Strapi Recipe!</a>
          </h1>
        </div>
        <div className={styles.recipeContainer}>
          {!loading && recipes ? (
            recipes.map((recipe, i) => {
              const { title, description } = recipe.attributes;
              return (
                <RecipeCard
                  key={i}
                  title={title}
                  bordered={true}
                  content={description}
                />
              );
            })
          ) : (
            <p>...Loading</p>
          )}
        </div>
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by Strapi
        </a>
      </footer>
    </div>
  );
}
```

Add the following styles to the `Home.module.css` file that is inside the `styles` directory

```
.recipeContainer {
  margin-top: 50px;
  display: flex;
  flex-wrap: wrap;
}

.item {
  margin: 5px;
}
```

Now, if we reload the project and navigate to `localhost` we should be able to see the recipe we added as shown below: 

![011-recipe-app-homepage.png](https://api-prod.strapi.io/uploads/011_recipe_app_homepage_b5a7961eba.png)

Now let's try and use the modal so we can click on the recipe and view more details about it.

We will just need to import our `RecipeModal` component, add a `handleClick` method and save our recipe in state.  The `index.js` component should now look as below:

```
import { useState } from 'react';
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useGetRecipe } from '../hooks/useGetRecipe';
import dynamic from 'next/dynamic';
const RecipeCard = dynamic(() => import('../components/RecipeCard'), {
  ssr: false,
});
const RecipeModal = dynamic(() => import('../components/RecipeModal'), {
  ssr: false,
});

export default function Home() {
  const { recipes, loading, error } = useGetRecipe();
  const [recipe, setRecipe] = useState(null);
  const [open, setOpen] = useState(false);

  const handleCardClick = (recipeToView) => {
    setRecipe(recipeToView);
    setOpen(true);
  };

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <div>
          <h1 className={styles.title}>
            Welcome to <a href="https://nextjs.org">Strapi Recipe!</a>
          </h1>
        </div>
        <div className={styles.recipeContainer}>
          {!loading && recipes ? (
            recipes.map((recipe, i) => {
              const { title, description } = recipe.attributes;
              return (
                <div
                  onClick={() => handleCardClick(recipe.attributes)}
                  key={i}
                  className={styles.item}
                >
                  <RecipeCard
                    title={title}
                    bordered={true}
                    content={description}
                  />
                </div>
              );
            })
          ) : (
            <p>...Loading</p>
          )}
        </div>
        <RecipeModal
          title={recipe?.title}
          open={open}
          setOpen={setOpen}
          description={recipe?.description}
          ingredients={recipe?.ingredients}
          instructions={recipe?.instructions}
        />
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by Strapi
        </a>
      </footer>
    </div>
  );
}
```

Now if we navigate to `localhost` and click on a recipe we should be able to see all of it's details.

![012-modal-for-recipe-details.png](https://api-prod.strapi.io/uploads/012_modal_for_recipe_details_a248c4b465.png)

We have connected our data from Strapi with our frontend components and everything seems to be working smoothly, feel free to add more recipes from the Strapi dashboard and watch them appear in the frontend. Next up, let's use our API connected to ChatGPT to enhance the functionality of our app.


## Enhancing users experience with AI suggestions

Let's enhance the functionality by adding an extra button to our modal allowing users to make the dish plant-based. We can do this by sending the ingredients of the recipe to our ChatGPT API, which will then provide us with suggestions to change them to plant-based alternatives.

First things first, let's set up the API call with `fetch` in the `utils` directory. Add the code below to `api.js`.


```
export async function fetchRecipeGPTData(ingredients) {
  try {
    const response = await fetch(`${baseUrl}/api/recipe-gpt/exampleAction`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        data: {
          input: `Here are the ingredients for a recipe ${ingredients}, provide me with a plant based version`,
        },
      }),
    });

    return await response.json();
  } catch (e) {
    console.error('Error fetching data:', error);
    throw error;
  }
}
```

As we can see, we are just passing the ingredients in with a simple prompt to GPT.

Next under the `hooks` directory create a file named `useRecipeGPTData.js` and pass in the below code

```
import { useState } from 'react';
import { fetchRecipeGPTData } from '../utils/api';

export function useRecipeGPTData() {
  const [data, setData] = useState();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState();

  const fetchData = async (ingredients) => {
    setIsLoading(true);
    try {
      const res = await fetchRecipeGPTData(ingredients);
      const dataToSave = res.data.message;

      setData(dataToSave);
    } catch (e) {
      setError(e);
    } finally {
      setIsLoading(false);
    }
  };
  return { data, fetchData, isLoading, setData, error };
}
```

Now we can use this hook to prompt GPT from anywhere in our app. 

All that's left to do is to connect this function to our modal so we can allow users to query it.

In `index.js`, import `useRecipeGPTData` and pass some of the functions in to the `RecipeModal`. The `index.js` file should now look like this 

```
import { useState } from 'react';
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useGetRecipe } from '../hooks/useGetRecipe';
import { useRecipeGPTData } from '../hooks/useRecipeGPTData';
import dynamic from 'next/dynamic';
const RecipeCard = dynamic(() => import('../components/RecipeCard'), {
  ssr: false,
});
const RecipeModal = dynamic(() => import('../components/RecipeModal'), {
  ssr: false,
});

export default function Home() {
  const { recipes, loading, error } = useGetRecipe();
  const { data, fetchData, isLoading, setData } = useRecipeGPTData();
  const [recipe, setRecipe] = useState(null);
  const [open, setOpen] = useState(false);

  const handleCardClick = (recipeToView) => {
    setRecipe(recipeToView);
    setOpen(true);
  };

  const handleCloseModal = () => {
    setData('');
    setOpen(false);
  };

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <div>
          <h1 className={styles.title}>
            Welcome to <a href="https://nextjs.org">Strapi Recipe!</a>
          </h1>
        </div>
        <div className={styles.recipeContainer}>
          {!loading && recipes ? (
            recipes.map((recipe, i) => {
              const { title, description } = recipe.attributes;
              return (
                <div
                  onClick={() => handleCardClick(recipe.attributes)}
                  key={i}
                  className={styles.item}
                >
                  <RecipeCard
                    title={title}
                    bordered={true}
                    content={description}
                  />
                </div>
              );
            })
          ) : (
            <p>...Loading</p>
          )}
        </div>
        <RecipeModal
          title={recipe?.title}
          open={open}
          handleCloseModal={handleCloseModal}
          description={recipe?.description}
          ingredients={recipe?.ingredients}
          instructions={recipe?.instructions}
          getRecipeGPTData={fetchData}
          recipeGPTData={data}
          isLoading={isLoading}
        />
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by Strapi
        </a>
      </footer>
    </div>
  );
}
```

Lastly, change the code in `RecipeModal` to display a button to query the API and a section for the plant-based version of the dish. The `RecipeModal` should now look like the one below:

```
import { Modal } from 'antd';

export default function RecipeModal({
  title,
  open,
  handleCloseModal,
  width = 1000,
  description,
  ingredients,
  instructions,
  getRecipeGPTData,
  recipeGPTData,
  isLoading,
}) {
  return (
    <div>
      <Modal
        title={title}
        centered
        open={open}
        onOk={handleCloseModal}
        onCancel={handleCloseModal}
        width={width}
      >
        <h3>Description</h3>
        <p>{description}</p>
        <h3>Ingredients</h3>
        <p>{ingredients}</p>
        {!isLoading ? (
          <button onClick={() => getRecipeGPTData(ingredients)}>
            Make plant based
          </button>
        ) : (
          <p>Loading...</p>
        )}

        <h3>Instructions</h3>
        <p>{instructions}</p>
        {recipeGPTData ? (
          <div>
            <h3>Plant based version</h3>
            <p>{recipeGPTData}</p>
          </div>
        ) : null}
      </Modal>
    </div>
  );
}
```

We should now be able to press the newly added button to generate a plant based version of the dish with our GPT powered API.

![013-add-ai-functionality.png](https://api-prod.strapi.io/uploads/013_add_ai_functionality_b5585d1c77.png)

## Conclusion
So now we have seen how to set up a Strapi API to serve content to our frontend (in our case, Next.js, a React.Js framework). We have also shown how to set up custom API endpoints and connect them to other technologies, such as ChatGPT. This can be used to make our apps appealing to a wider audience by customizing the functionality. 

Hopefully, this has inspired us to explore this area further and think of new ways to improve our apps.

## Resources
* [Repo to the full code](https://github.com/gitChimp88/strapi-tutorial)
* [Guide on authenticating requests with the REST API](https://strapi.io/blog/guide-on-authenticating-requests-with-the-rest-api)
* [Build A ChatGPT Clone with Strapi, Open AI, LangChain, and Next.js](https://strapi.io/blog/build-a-chat-gpt-clone-with-strapi-openai-langchain-and-nextjs)
* [Next.Js Image Configuration Option(domains)](https://nextjs.org/docs/app/api-reference/components/image#domains)