DEV Community

Abdullah Al Numan
Abdullah Al Numan

Posted on • Updated on

Strapi with Refine Part I: An Introduction to Refine's Strapi v4 Connector

Introduction

Refine is a React based meta framework used to quickly build data intensive web applications. It comes with an extensible headless core and with the help of supplementary packages offers integrations with a myriad of frontend and backend technologies.

Refine supports integration with a Strapi CMS by means of a data provider package that facilitates communication with the Strapi endpoints. A data provider in Refine acts as a two-way adapter that gets installed on top of the headless core. Currently, Refine has data providers for Strapi version 4 REST API, as well as Strapi GraphQL APIs.

This post provides an introduction to integrating a Strapi backend API to a Refine frontend app with Refine's Strapi v4 adapter. It discusses in siginificant depth Refine's ecosystem of data hooks and components, and where they are posited in Refine's architecture. It also explains the use of Refine's collection of Ant Design supported data hooks and components in quickly building React based CRUD applications.

Overview

We begin with a discussion on Refine's React context and hooks based architecture, some underlying concepts and the libraries at play for performing and managing data heavy tasks in Refine. We aim to get a fair understanding of what a data provider in Refine is and where it fits in Refine's cascade of React contexts accessible via hooks.

In the later part of the post, we demonstrate how to initialize a Refine app named Strapi Refine Blog with Strapi v4 data connector. We examine in some depth the methods in Refine's Strapi v4 data provider object that enables CRUD operations on a Strapi backend API.

The backend API for Strapi Refine Blog is already created and hosted on the Strapi Cloud here. Towards the end, we show how to use its Strapi API credentials inside the Refine app in order to connect to its endpoints.

Refine Architecture

Refine's architecture lays different app concerns into cascades of React contexts which are passed their respective provider objects. It separates authentication, authorization, data fetching and state management, notification, real time pub/sub, audit logging and internationalization into distinct cascading layers.

Refine Has an Extensible Headless Core

Refine has a headless core. The core encapsulates the app's context layers which operate with the help of providers and hooks. Being headless, Refine's core offers surface for seamless integration with various backend API services and UI libraries.

Providers in Refine

A provider object in Refine contains methods that address the concerns on a particular layer. It acts as a bridge between the core and the API surface of the library it integrates with.

For instance, Refine accepts a data provider object with methods for connecting to and fetching data with a backend API service. Similarly, it has providers with methods for authentication, authorization, and other concerns of the app.

Refine Data Rrovider

Server side data fetching and associated tasks make up Refine's data layer. And a data provider object with methods for handling CRUD (create, read, update and delete actions) are passed to its context so that they can be accessed down the cascade from inside the app's pages and components. The data provider methods of a particular API service provider (for instance, Strapi REST) are defined tailored according to their own data fetching API surface.

The provider object is passed to its context via the <Refine /> component, which forms the core of a Refine app:

<Refine
    // ... other props
    dataProvider={dataProvider}
>

Enter fullscreen mode Exit fullscreen mode

Refine Data Provider Shape

Refine needs a specific set of data provider methods for dealing with CRUD actions in the data layer. The dataProvider object should have the following methods:

import { DataProvider } from "@refinedev/core";

const dataProvider: DataProvider = {

  // required methods
  getList: ({ resource, pagination, sorters, filters, meta }) => Promise,
  create: ({ resource, variables, meta }) => Promise,
  update: ({ resource, id, variables, meta }) => Promise,
  deleteOne: ({ resource, id, variables, meta }) => Promise,
  getOne: ({ resource, id, meta }) => Promise,
  getApiUrl: () => "",

  // optional methods
  getMany?: ({ resource, ids, meta }) => Promise,
  createMany?: ({ resource, variables, meta }) => Promise,
  deleteMany?: ({ resource, ids, variables, meta }) => Promise,
  updateMany?: ({ resource, ids, variables, meta }) => Promise,
  custom?: ({ url, method, filters, sorters, payload, query, headers, meta }) => Promise,
};
Enter fullscreen mode Exit fullscreen mode

As you can refer, it is mandatory to define the getOne, getList, create, update and deleteOne methods that are essential to CRUD applications. We are able to get the API url with the getApiUrl method.

The "many" versions are optional. And we are able to add a custom method in the case we need to handle things differently at some point.

Uncontrained Backend API Integration

Refine's headless architecture not only decouples the core from a UI design system and implementation, it also distances itself from strictly binding to certain data providers. This lets us pass any data provider of our choice that conforms to the above shape. For the demo app used in this post, we'll be passing the Strapi v4 data provider and as we'll elaborate in a later section, Strapi v4 is a REST API connector that lets a Refine app connect to Strapi REST endpoints.

Hooks in Refine

Provider methods for a layer in Refine are accessed from pages and components via a standard set of core hooks that correspond to these methods.

For example, the data provider object above -- identified as dataProvider -- has a create method that helps perform a create action on a resource hosted at an API endpoint. And dataProvider.create can be accessed from a component down the Refine cascade via the useCreate Refine core hook.

Core Hooks vs Higher Level Hooks

Refine's headless core comes with a set of core hooks for each layer. These core or low level hooks are decoupled from any particular UI library and can be extended with support for popular design systems and frameworks (e.g. Ant Design) with the help of adapter packages built by Refine and the community. Extension packages offer higher level hooks that integrate and are enhanced with the functionalities of UI components particular to the design system.

Refinedev Ant Design Support Package

For example, the useCreate() core data hook is used inside the useForm() high level hook by Refine's adapter for Ant Design in the Refinedev Ant Design package. The enhanced useForm() hook is tailored to be used with an Ant Design <Form /> component.

The Ant Design support package contains other high level data hooks for quickly building sophisticated components such as tables, lists, forms, data charts, etc. The full API reference can be found here.

Refine Under the Hood

Refine employs React Query in its data hooks to handle data fetching, caching, state management and error handling.

For forms, it relies on React Hook Form with its Refinedev React Hook Form adapter package to manage form logic and error handling. For rendering tables, it uses Tanstack React Table via the Refinedev React Table support package.

With this fair bit of understanding about how Refine works, we can now go about initializing a Refine app that will be geared to connect with a Strapi CMS.

Setting Up Strapi 4 Data Provider in a Refine App

In this section, we go through the steps for initializing a Refine app with Strapi v4 data provider. Refine lets us choose a data provider at project initialization. So, we are going to opt for Strapi 4 REST adapter from a range of other backend integration options.

Pre-requisites

Environment

  • Node.js and npm: You should have Node.js and npm installed in your system. We recommend Node versions > 18.

Knowledge

  • React with TypeScript: Refine is written in TypeScript and by default creates a React app with supported TS types. So, we assume you already have enough experience building React apps with TypeScript.
  • Strapi 4: We also assume you have a fair understanding and some experience building a Strapi 4 backend API.
  • Ant Design: This app uses Ant Design support for Refine. We expect you are familiar with Ant Design React ecosystem, particularly the components and layouts related to CRUD operations, such as <Form /> and buttons.

Initializing a Strapi Backed Frontend App with Refine

Let's now go ahead and create the Refine app. Follow the steps below:

  1. Open a Terminal and run the npm create command for creating Refine apps. We are naming our app strapi-refine-blog:
npm create refine-app@latest strapi-refine-blog
Enter fullscreen mode Exit fullscreen mode

This will open the Refine CLI shell where you'll be presented with options to interactively choose a tooling system and integration packages we want in the app.

  1. Make sure you have chosen the following in the end:
 ✔ Downloaded remote source successfully.
 ✔ Choose a project template · refine-vite
 ✔ What would you like to name your project?: · strapi-refine-blog
 ✔ Choose your backend service to connect: · data-provider-strapi-v4
 ✔ Do you want to use a UI Framework?: · antd
 ✔ Do you want to add example pages?: · no
 ✔ Do you need i18n (Internationalization) support?: · no
 ✔ Choose a package manager: · npm
Enter fullscreen mode Exit fullscreen mode

Notice that we have selected Strapi v4 as our data provider. And for UI components, we have Ant Design. We also made sure we didn't include example pages. This is because we would like to begin building the pages from scratch.

Code Inspection

Refine initialization scaffolds an application with a directory structure like below:

Image description

Please feel free to navigate around and try to make sense of what's going on. The main action happens inside the <Refine /> component rendered in src/App.tsx.

Our choice of Strapi v4 data provider placed Refine's connector for Strapi 4 among Node modules under the node_modules directory in node_modules/@refinedev/strapi-v4/src/index.ts. It also produced a src/constants.ts file that stores API credentials provided by a Strapi backend.

In the sections ahead, we examine what the Strapi v4 package brings us with regards to connecting to resources on Strapi endpoints and performing actions on them. We also replace default credentials in the constants.ts file with that of our Strapi Refine Blog backend.

Refine's Strapi v4 Data Provider: A Sneak Peek

Refine's Strapi v4 package gives us the following dataProvider function inside @refinedev/strapi-v4:

import { DataProvider as IDataProvider, HttpError } from "@refinedev/core";
import { AxiosInstance } from "axios";
import { stringify } from "qs";
import {
    axiosInstance,
    generateFilter,
    generateSort,
    normalizeData,
    transformHttpError,
} from "./utils";

export const DataProvider = (
    apiUrl: string,
    httpClient: AxiosInstance = axiosInstance,
): Required =&gt; ({
    getList: async ({ resource, pagination, filters, sorters, meta }) =&gt; {
        const url = `${apiUrl}/${resource}`;

        const {
            current = 1,
            pageSize = 10,
            mode = "server",
        } = pagination ?? {};

        const locale = meta?.locale;
        const fields = meta?.fields;
        const populate = meta?.populate;
        const publicationState = meta?.publicationState;

        const querySorters = generateSort(sorters);
        const queryFilters = generateFilter(filters);

        const query = {
            ...(mode === "server"
                ? {
                      "pagination[page]": current,
                      "pagination[pageSize]": pageSize,
                  }
                : {}),
            locale,
            publicationState,
            fields,
            populate,
            sort: querySorters.length &gt; 0 ? querySorters.join(",") : undefined,
        };

        const { data } = await httpClient.get(
            `${url}?${stringify(query, {
                encodeValuesOnly: true,
            })}&amp;${queryFilters}`,
        );

        return {
            data: normalizeData(data),
            // added to support pagination on client side when using endpoints that provide only data (see https://github.com/refinedev/refine/issues/2028)
            total: data.meta?.pagination?.total || normalizeData(data)?.length,
        };
    },

    getMany: async ({ resource, ids, meta }) =&gt; {
        const url = `${apiUrl}/${resource}`;

        const locale = meta?.locale;
        const fields = meta?.fields;
        const populate = meta?.populate;
        const publicationState = meta?.publicationState;

        const queryFilters = generateFilter([
            {
                field: "id",
                operator: "in",
                value: ids,
            },
        ]);

        const query = {
            locale,
            fields,
            populate,
            publicationState,
            "pagination[pageSize]": ids.length,
        };

        const { data } = await httpClient.get(
            `${url}?${stringify(query, {
                encodeValuesOnly: true,
            })}&amp;${queryFilters}`,
        );

        return {
            data: normalizeData(data),
        };
    },

    create: async ({ resource, variables }) =&gt; {
        const url = `${apiUrl}/${resource}`;

        let dataVariables: any = { data: variables };

        if (resource === "users") {
            dataVariables = variables;
        }

        try {
            const { data } = await httpClient.post(url, dataVariables);
            return {
                data,
            };
        } catch (error) {
            const httpError = transformHttpError(error);

            throw httpError;
        }
    },

    update: async ({ resource, id, variables }) =&gt; {
        const url = `${apiUrl}/${resource}/${id}`;

        let dataVariables: any = { data: variables };

        if (resource === "users") {
            dataVariables = variables;
        }

        try {
            const { data } = await httpClient.put(url, dataVariables);
            return {
                data,
            };
        } catch (error) {
            const httpError = transformHttpError(error);

            throw httpError;
        }
    },

    updateMany: async ({ resource, ids, variables }) =&gt; {
        const errors: HttpError[] = [];

        const response = await Promise.all(
            ids.map(async (id) =&gt; {
                const url = `${apiUrl}/${resource}/${id}`;

                let dataVariables: any = { data: variables };

                if (resource === "users") {
                    dataVariables = variables;
                }

                try {
                    const { data } = await httpClient.put(url, dataVariables);
                    return data;
                } catch (error) {
                    const httpError = transformHttpError(error);

                    errors.push(httpError);
                }
            }),
        );

        if (errors.length &gt; 0) {
            throw errors;
        }

        return { data: response };
    },

    createMany: async ({ resource, variables }) =&gt; {
        const errors: HttpError[] = [];

        const response = await Promise.all(
            variables.map(async (param) =&gt; {
                try {
                    const { data } = await httpClient.post(
                        `${apiUrl}/${resource}`,
                        {
                            data: param,
                        },
                    );
                    return data;
                } catch (error) {
                    const httpError = transformHttpError(error);

                    errors.push(httpError);
                }
            }),
        );

        if (errors.length &gt; 0) {
            throw errors;
        }

        return { data: response };
    },

    getOne: async ({ resource, id, meta }) =&gt; {
        const locale = meta?.locale;
        const fields = meta?.fields;
        const populate = meta?.populate;

        const query = {
            locale,
            fields,
            populate,
        };

        const url = `${apiUrl}/${resource}/${id}?${stringify(query, {
            encode: false,
        })}`;

        const { data } = await httpClient.get(url);

        return {
            data: normalizeData(data),
        };
    },

    deleteOne: async ({ resource, id }) =&gt; {
        const url = `${apiUrl}/${resource}/${id}`;

        const { data } = await httpClient.delete(url);

        return {
            data,
        };
    },

    deleteMany: async ({ resource, ids }) =&gt; {
        const response = await Promise.all(
            ids.map(async (id) =&gt; {
                const { data } = await httpClient.delete(
                    `${apiUrl}/${resource}/${id}`,
                );
                return data;
            }),
        );
        return { data: response };
    },

    getApiUrl: () =&gt; {
        return apiUrl;
    },

    custom: async ({
        url,
        method,
        filters,
        sorters,
        payload,
        query,
        headers,
    }) =&gt; {
        let requestUrl = `${url}?`;

        if (sorters) {
            const sortQuery = generateSort(sorters);
            if (sortQuery.length &gt; 0) {
                requestUrl = `${requestUrl}&amp;${stringify({
                    sort: sortQuery.join(","),
                })}`;
            }
        }

        if (filters) {
            const filterQuery = generateFilter(filters);
            requestUrl = `${requestUrl}&amp;${filterQuery}`;
        }

        if (query) {
            requestUrl = `${requestUrl}&amp;${stringify(query)}`;
        }

        let axiosResponse;
        switch (method) {
            case "put":
            case "post":
            case "patch":
                axiosResponse = await httpClient[method](url, payload, {
                    headers,
                });
                break;
            case "delete":
                axiosResponse = await httpClient.delete(url, {
                    data: payload,
                    headers: headers,
                });
                break;
            default:
                axiosResponse = await httpClient.get(requestUrl, { headers });
                break;
        }

        const { data } = axiosResponse;

        return Promise.resolve({ data });
    },
});
Enter fullscreen mode Exit fullscreen mode

If you examine the above snippet closely, you can notice that the Strapi v4 data provider uses an Axios instance, axiosInstance, as API client to connect to a Strapi backend. It gives us a dataProvider function that accepts apiUrl and the Axios httpClient to produce the data provider object for Strapi.

This data provider object is generic to any Strapi 4 REST API. It has all the methods needed to perform CRUD actions on a typical Strapi 4 REST API.

Application specific details particular to our backend such as resource and query related configurations like pagination, filters, sorters, id, etc. are being accepted as parameters to these methods. For example, the getList method accepts resource, pagination, filters, sorters and meta which are needed by React Query for caching, state management and request handling. They have to be passed when invoking dataProvider.getList via a corresponding data hook, like useList().

This way, Refine's core and higher level hooks help configure CRUD actions when they are performed from inside a UI component.

How to Connect Strapi REST API to a Refine App

With the above Strapi v4 data provider availed to the app, we can now go about connecting it to our backend API.

The Strapi Backend

For this demo, we are going to connect to Strapi Refine Blog -- a Strapi Cloud hosted simple blog app that comes already configured with necessary permissions and API credentials.

Here's a quick run down of the main points:

  1. The blog contains two resources: posts and categories.
  2. Both resources allow the following actions: create, find, findOne, update and delete to a public role.
  3. Necessary permissions are set from the Strapi Admin panel for public role to be able to access these resources and perform the above actions.
  4. An API token has been generated and stored for use by HTTPS clients.

Adding Strapi API Credentials to a Refine App

Let's update the constants inside src/constants.ts with the below API credentials for Strapi Refine Blog:

export const API_URL = "https://joyful-frog-8daba895c7.strapiapp.com";
export const TOKEN_KEY = "ab2c5697a17afa8c4dadfa2d89995e137a3cb5db05812d007e827aa5a9bc6b88f7d1c6b7d6261a0a2338350a42c0a6f8b5d1b54242122b59c88b81770f50bbafeb6b2598e90f552eff9379fab31ef8c7d454606c05450114607c7a46bf5728a7d04a4d1ceb0c6fd988d6ce7f55c33c6dba3b8e24b11956e394f2660dcf367004";
Enter fullscreen mode Exit fullscreen mode

Ideally, you'd want them stored as environment variables. However, we'll forego this step for the purpose of this simple demo.

Run the Refine Dev Server

Alright, with the Refine app now set to connect with a Strapi backend, we can go ahead and run the app with the following npm command:

npm run dev
Enter fullscreen mode Exit fullscreen mode

This spins up the Refine application server at http://localhost:5173. When we open a browser and visit the app, we should be greeted with the Refine Welcome page:

Image description

Summary

In this post, we got familiar with Refine's context and hooks based architecture and learned about how data providers, corresponding hooks and data fetching/rendering libraries work together in a Refine app.

After initializing a Refine app geared to interact with a Strapi 4 backend, we inspected the scaffolded code to gain useful insight into how Refine's Strapi 4 backend adapter helps connect to a Strapi REST API and enables CRUD operations with a set of methods defined to implement CRUD actions. We also updated the Refine app with credentials for our Strapi Refine Blog CMS that is hosted on Strapi Cloud.

In Strapi with Refine Part II: How to Add CRUD Pages to a Strapi Backed Refine App, we reveal how to build the resource pages for all CRUD actions with Ant Design supported Refine hooks and components.

Top comments (0)