How to Build a CRUD App with React and a Headless CMS
Published: 2021-06-17 14:33:58


For many years, web projects have used Content Management Systems (CMS) to create and manage content, store it in a database, and display it using server-side rendered programming languages. WordPress, Drupal, Joomla are well-known applications used for this purpose.

One of the issues the traditional CMSes have is that the backend is coupled to the presentation layer. So, developers are forced to use a certain programming language and framework to display the information. This makes it difficult to reuse the same content on other platforms, like mobile applications, and here is where headless CMSes can provide many benefits.

A Headless CMS is a Content Management System not tied to a presentation layer. It's built as a content repository that exposes information through an API, which can be accessed from different devices and platforms. A headless CMS is designed to store and expose organized, structured content without concern over where and how it's going to be presented to users.

This decoupling of presentation and storage offers several advantages:

- **Flexibility:** Developers can present content on different devices and platforms using the same single source of truth.

- **Scalability:** Headless CMSes allow your content library to grow without affecting the frontend of your app and vice-versa.

- **Security:** You can expose only the data you want on the frontend and have a completely separate login for web administrators who edit the content.

- **Speed:** As data is consumed through an API, you can dynamically display data on pages without re-rendering the content.

In this article, I will show you how to create a Pet Adoption CRUD application. You will use a headless CMS called [Strapi](<https://strapi.io/>) for the backend, and [React](https://reactjs.org/) with [Redux](https://redux.js.org/) for the frontend. The application will display a list of pets, with details related to each, and you will be able to add, edit or delete pets from the list.

## Planning the Application

**CRUD** stands for Create, Read, Update and Delete. CRUD applications are typically composed of pages or endpoints that allow users to interact with entities stored in a database. Most applications deployed to the internet are at least partially CRUD applications, and many are exclusively CRUD apps.

This example application will have `Pet` entities, with details about each pet, and you will be able to execute CRUD operations on them. The application will have a screen with a list of pets and a link to another screen to add a pet to the list. It will also include a button to update pet details and another one to remove a pet from the database.

## Building the Backend Data Structure

To create, manage and store the data related to the pets, we will use [Strapi](<https://strapi.io/>), an open-source headless CMS built on Node.js.

Strapi allows you to create *content types* for the entities in your app and a dashboard that can be configured depending on your needs. It exposes entities via [its Content API](<https://strapi.io/documentation/developer-docs/latest/developer-resources/content-api/content-api.html>), which you'll use to populate the frontend.

If you want to see the generated code for the Strapi backend, you can download it from [this GitHub repository](https://github.com/fgiuliani/pet-adoption-backend).

To start creating the backend of your application, install Strapi and create a new project:

```

npx create-strapi-app pet-adoption-backend --quickstart

```

This will install Strapi, download all the dependencies and create an initial project called `pet-adoption-backend`.

The `--quickstart` flag is appended to instruct Strapi to use SQLite for the database. If you don't use this flag, you should install a local database to link to your Strapi project. You can take a look at [Strapi's installation documentation](<https://strapi.io/documentation/developer-docs/latest/setup-deployment-guides/installation.html>) for more details and different installation options.

After all the files are downloaded and installed and the project is created, a registration page will be opened at the URL [http://localhost:1337/admin/auth/register-admin](http://localhost:1337/admin/auth/register-admin).

![Register Admin](https://imgur.com/zjbDZY7.png)

Complete the fields on the page to create an Administrator user.

After this, you will be redirected to your dashboard. From this page, you can manage all the data and configuration of your application.

![Strapi Dashboard](https://imgur.com/6pnRZv7.png)

You will see that there is already a `Users` collection type. To create a new collection type, go to the **Content-Types Builder** link on the left menu and click **+ Create new collection type**. Name it *pet*.

![Create New Content Type](https://imgur.com/FbItrVi.png)

After that, add the fields to the content type, and define the name and the type for each one. For this pet adoption application, include the following fields:

- `name` (Text - Short Text)

- `animal` (Enumeration: Cat - Dog - Bird)

- `breed` (Text - Short Text)

- `location` (Text - Short Text)

- `age` (Number - Integer)

- `sex` (Enumeration: Male - Female)

![Pet Content Type](https://imgur.com/LaXTXTe.png)

For each field, you can define different parameters by clicking **Advanced Settings**. Remember to click **Save** after defining each entity.

Even though we will create a frontend for our app, you can also add new entries here in your Strapi Dashboard. On the left menu, go to the `Pets` collection type, and click **Add New Pet**.

![Add New Pet](https://imgur.com/0lpoprs.png)

New entries are saved as "drafts" by default, so to see the pet you just added, you need to publish it.

### Using the Strapi REST API

Strapi gives you a complete REST API out of the box. If you want to make the pet list public for viewing (not recommended for creating, editing, or updating), go to **Settings**, click **Roles**, and edit **Public**. Enable **find** and **findone** for the **Public** role.

![Public Role](https://imgur.com/IAMvfAH.png)

Now you can call the [http://localhost:1337/pets](http://localhost:1337/pets) REST endpoint from your application to list all pets, or you can call `http://localhost:1337/pets/[petID]` to get a specific pet's details.

![REST API Get](https://imgur.com/KMEmi0Y.png)

### Using the Strapi GraphQL Plugin

If instead of using the REST API, you want to use a [GraphQL](https://graphql.org/) endpoint, you can add one. On the left menu, go to **Marketplace**. A list of plugins will be displayed. Click **Download** for the GraphQL plugin.

![GraphQL Plugin](https://imgur.com/GYeKxFm.png)

Once the plugin is installed, you can go to [http://localhost:1337/graphql](http://localhost:1337/graphql) to view and test the endpoint.

![GraphQL Query](<https://raw.githubusercontent.com/fgiuliani/pet-adoption/main/images/graphql-query.png>)

## Building the Frontend

For the Pet List, Add Pet, Update Pet, and Delete Pet features from the application, you will use React with [Redux](<https://redux.js.org/>). Redux is a state management library. It needs an intermediary tool, `react-redux`, to enable communication between the Redux store and the React application.

As my primary focus is to demonstrate creating a CRUD application using a headless CMS, I won't show you all the styling in this tutorial, but to get the code, you can fork [this GitHub repository](<https://github.com/fgiuliani/pet-adoption>).

First, create a new React application:

```

npx create-react-app pet-adoption

```

Once you've created your React app, install the required npm packages:

```

npm install react-router-dom @reduxjs/toolkit react-redux axios

```

- `react-router-dom` handles the different pages.

- `@reduxjs/toolkit` and `react-redux` add the redux store to the application.

- `axios` connects to the Strapi REST API.

Inside the `src` folder, create a helper `http.js` file, with code that will be used to connect to Strapi API:

```

import axios from "axios";

export default axios.create({

 baseURL: "http://localhost:1337",

 headers: {

   "Content-type": "application/json",

 },

});

```

Create a `petsService.js` file with helper methods for all the CRUD operations inside a new folder called `pets`:

```

import http from "../http";

class PetsService {

 getAll() {

   return http.get("/pets");

 }

 get(id) {

   return http.get(`/pets/${id}`);

 }

 create(data) {

   return http.post("/pets", data);

 }

 update(id, data) {

   return http.put(`/pets/${id}`, data);

 }

 delete(id) {

   return http.delete(`/pets/${id}`);

 }

}

export default new PetsService();

```

Redux uses *actions* and *reducers*. According to the [Redux documentation](https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers#designing-actions), actions are "an event that describes something that happened in the application." Reducers are functions that take the current state and an action as arguments and return a new state result.

To create actions, you first need to define action types. Create a file inside the `pets` folder called `actionTypes.js`:

```

export const CREATE_PET = "CREATE_PET";

export const RETRIEVE_PETS = "RETRIEVE_PETS";

export const UPDATE_PET = "UPDATE_PET";

export const DELETE_PET = "DELETE_PET";

```

Create an `actions.js` file in the same folder:

```

import {

 CREATE_PET,

 RETRIEVE_PETS,

 UPDATE_PET,

 DELETE_PET,

} from "./actionTypes";

import PetsService from "./petsService";

export const createPet =

 (name, animal, breed, location, age, sex) => async (dispatch) => {

   try {

     const res = await PetsService.create({

       name,

       animal,

       breed,

       location,

       age,

       sex,

     });

     dispatch({

       type: CREATE_PET,

       payload: res.data,

     });

     return Promise.resolve(res.data);

   } catch (err) {

     return Promise.reject(err);

   }

 };

export const retrievePets = () => async (dispatch) => {

 try {

   const res = await PetsService.getAll();

   dispatch({

     type: RETRIEVE_PETS,

     payload: res.data,

   });

 } catch (err) {

   console.log(err);

 }

};

export const updatePet = (id, data) => async (dispatch) => {

 try {

   const res = await PetsService.update(id, data);

   dispatch({

     type: UPDATE_PET,

     payload: data,

   });

   return Promise.resolve(res.data);

 } catch (err) {

   return Promise.reject(err);

 }

};

export const deletePet = (id) => async (dispatch) => {

 try {

   await PetsService.delete(id);

   dispatch({

     type: DELETE_PET,

     payload: { id },

   });

 } catch (err) {

   console.log(err);

 }

};

```

To create your reducers, add a new `reducers.js` file in the same folder:

```

import {

 CREATE_PET,

 RETRIEVE_PETS,

 UPDATE_PET,

 DELETE_PET,

} from "./actionTypes";

const initialState = [];

function petReducer(pets = initialState, action) {

 const { type, payload } = action;

 switch (type) {

   case CREATE_PET:

     return [...pets, payload];

   case RETRIEVE_PETS:

     return payload;

   case UPDATE_PET:

     return pets.map((pet) => {

       if (pet.id === payload.id) {

         return {

           ...pet,

           ...payload,

         };

       } else {

         return pet;

       }

     });

   case DELETE_PET:

     return pets.filter(({ id }) => id !== payload.id);

   default:

     return pets;

 }

}

export default petReducer;

```

Now that you have the actions and the reducers, create a `store.js` file in the `src` folder:

```

import { configureStore } from "@reduxjs/toolkit";

import petReducer from "./pets/reducers";

export default configureStore({

 reducer: {

   pets: petReducer,

 },

});

```

Here you are configuring the Redux store and adding a `petReducer` function to mutate the state. You're setting the store to be accessible from anywhere in your application. After this, wrap the whole app inside the store using the Redux wrapper.

Your `index.js` file should now look like this:

```

import App from "./App";

import { Provider } from "react-redux";

import React from "react";

import ReactDOM from "react-dom";

import store from "./store";

ReactDOM.render(

 <Provider store={store}>

   <App />

 </Provider>,

 document.getElementById("root")

);

```

Create a new component called `PetList.jsx`:

```

import React, { Component } from "react";

import { connect } from "react-redux";

import { Link } from "react-router-dom";

import { retrievePets, deletePet } from "../pets/actions";

class PetList extends Component {

 componentDidMount() {

   this.props.retrievePets();

 }

 removePet = (id) => {

   this.props.deletePet(id).then(() => {

     this.props.retrievePets();

   });

 };

 render() {

   const { pets } = this.props;

   return (

     <div className="list row">

       <div className="col-md-6">

         <h4>Pet List</h4>

         <div>

           <Link to="/add-pet">

             <button className="button-primary">Add pet</button>

           </Link>

         </div>

         <table className="u-full-width">

           <thead>

             <tr>

               <th>Name</th>

               <th>Animal</th>

               <th>Breed</th>

               <th>Location</th>

               <th>Age</th>

               <th>Sex</th>

               <th>Actions</th>

             </tr>

           </thead>

           <tbody>

             {pets &&

               pets.map(

                 ({ id, name, animal, breed, location, age, sex }, i) => (

                   <tr key={i}>

                     <td>{name}</td>

                     <td>{animal}</td>

                     <td>{breed}</td>

                     <td>{location}</td>

                     <td>{age}</td>

                     <td>{sex}</td>

                     <td>

                       <button onClick={() => this.removePet(id)}>

                         Delete

                       </button>

                       <Link to={`/edit-pet/${id}`}>

                         <button>Edit</button>

                       </Link>

                     </td>

                   </tr>

                 )

               )}

           </tbody>

         </table>

       </div>

     </div>

   );

 }

}

const mapStateToProps = (state) => {

 return {

   pets: state.pets,

 };

};

export default connect(mapStateToProps, { retrievePets, deletePet })(PetList);

```

You will use this component in your `App.js` file, displaying it on the homepage of the app.

Now create another file, `AddPet.jsx`, with a component to add a pet to the list:

```

import React, { Component } from "react";

import { connect } from "react-redux";

import { createPet } from "../pets/actions";

import { Redirect } from "react-router-dom";

class AddPet extends Component {

 constructor(props) {

   super(props);

   this.onChangeName = this.onChangeName.bind(this);

   this.onChangeAnimal = this.onChangeAnimal.bind(this);

   this.onChangeBreed = this.onChangeBreed.bind(this);

   this.onChangeLocation = this.onChangeLocation.bind(this);

   this.onChangeAge = this.onChangeAge.bind(this);

   this.onChangeSex = this.onChangeSex.bind(this);

   this.savePet = this.savePet.bind(this);

   this.state = {

     name: "",

     animal: "",

     breed: "",

     location: "",

     age: "",

     sex: "",

     redirect: false,

   };

 }

 onChangeName(e) {

   this.setState({

     name: e.target.value,

   });

 }

 onChangeAnimal(e) {

   this.setState({

     animal: e.target.value,

   });

 }

 onChangeBreed(e) {

   this.setState({

     breed: e.target.value,

   });

 }

 onChangeLocation(e) {

   this.setState({

     location: e.target.value,

   });

 }

 onChangeAge(e) {

   this.setState({

     age: e.target.value,

   });

 }

 onChangeSex(e) {

   this.setState({

     sex: e.target.value,

   });

 }

 savePet() {

   const { name, animal, breed, location, age, sex } = this.state;

   this.props.createPet(name, animal, breed, location, age, sex).then(() => {

     this.setState({

       redirect: true,

     });

   });

 }

 render() {

   const { redirect } = this.state;

   if (redirect) {

     return <Redirect to="/" />;

   }

   return (

     <div className="submit-form">

       <div>

         <div className="form-group">

           <label htmlFor="name">Name</label>

           <input

             type="text"

             className="form-control"

             id="name"

             required

             value={this.state.name}

             onChange={this.onChangeName}

             name="name"

           />

         </div>

         <div className="form-group">

           <label htmlFor="animal">Animal</label>

           <input

             type="text"

             className="form-control"

             id="animal"

             required

             value={this.state.animal}

             onChange={this.onChangeAnimal}

             name="animal"

           />

         </div>

         <div className="form-group">

           <label htmlFor="breed">Breed</label>

           <input

             type="text"

             className="form-control"

             id="breed"

             required

             value={this.state.breed}

             onChange={this.onChangeBreed}

             name="breed"

           />

         </div>

         <div className="form-group">

           <label htmlFor="location">Location</label>

           <input

             type="text"

             className="form-control"

             id="location"

             required

             value={this.state.location}

             onChange={this.onChangeLocation}

             name="location"

           />

         </div>

         <div className="form-group">

           <label htmlFor="age">Age</label>

           <input

             type="text"

             className="form-control"

             id="age"

             required

             value={this.state.age}

             onChange={this.onChangeAge}

             name="age"

           />

         </div>

         <div className="form-group">

           <label htmlFor="sex">Sex</label>

           <input

             type="text"

             className="form-control"

             id="sex"

             required

             value={this.state.sex}

             onChange={this.onChangeSex}

             name="sex"

           />

         </div>

         <button onClick={this.savePet} className="btn btn-success">

           Submit

         </button>

       </div>

     </div>

   );

 }

}

export default connect(null, { createPet })(AddPet);

```

This component will add a pet to the state.

Now, create an `EditPet.jsx` file:

```

import React, { Component } from "react";

import { connect } from "react-redux";

import { updatePet } from "../pets/actions";

import { Redirect } from "react-router-dom";

import PetService from "../pets/petsService";

class EditPet extends Component {

 constructor(props) {

   super(props);

   this.onChangeName = this.onChangeName.bind(this);

   this.onChangeAnimal = this.onChangeAnimal.bind(this);

   this.onChangeBreed = this.onChangeBreed.bind(this);

   this.onChangeLocation = this.onChangeLocation.bind(this);

   this.onChangeAge = this.onChangeAge.bind(this);

   this.onChangeSex = this.onChangeSex.bind(this);

   this.savePet = this.savePet.bind(this);

   this.state = {

     currentPet: {

       name: "",

       animal: "",

       breed: "",

       location: "",

       age: "",

       sex: "",

     },

     redirect: false,

   };

 }

 componentDidMount() {

   this.getPet(window.location.pathname.replace("/edit-pet/", ""));

 }

 onChangeName(e) {

   const name = e.target.value;

   this.setState(function (prevState) {

     return {

       currentPet: {

         ...prevState.currentPet,

         name: name,

       },

     };

   });

 }

 onChangeAnimal(e) {

   const animal = e.target.value;

   this.setState(function (prevState) {

     return {

       currentPet: {

         ...prevState.currentPet,

         animal: animal,

       },

     };

   });

 }

 onChangeBreed(e) {

   const breed = e.target.value;

   this.setState(function (prevState) {

     return {

       currentPet: {

         ...prevState.currentPet,

         breed: breed,

       },

     };

   });

 }

 onChangeLocation(e) {

   const location = e.target.value;

   this.setState(function (prevState) {

     return {

       currentPet: {

         ...prevState.currentPet,

         location: location,

       },

     };

   });

 }

 onChangeAge(e) {

   const age = e.target.value;

   this.setState(function (prevState) {

     return {

       currentPet: {

         ...prevState.currentPet,

         age: age,

       },

     };

   });

 }

 onChangeSex(e) {

   const sex = e.target.value;

   this.setState(function (prevState) {

     return {

       currentPet: {

         ...prevState.currentPet,

         sex: sex,

       },

     };

   });

 }

 getPet(id) {

   PetService.get(id).then((response) => {

     this.setState({

       currentPet: response.data,

     });

   });

 }

 savePet() {

   this.props

     .updatePet(this.state.currentPet.id, this.state.currentPet)

     .then(() => {

       this.setState({

         redirect: true,

       });

     });

 }

 render() {

   const { redirect, currentPet } = this.state;

   if (redirect) {

     return <Redirect to="/" />;

   }

   return (

     <div className="submit-form">

       <div>

         <div className="form-group">

           <label htmlFor="name">Name</label>

           <input

             type="text"

             className="form-control"

             id="name"

             required

             value={currentPet.name}

             onChange={this.onChangeName}

             name="name"

           />

         </div>

         <div className="form-group">

           <label htmlFor="animal">Animal</label>

           <input

             type="text"

             className="form-control"

             id="animal"

             required

             value={currentPet.animal}

             onChange={this.onChangeAnimal}

             name="animal"

           />

         </div>

         <div className="form-group">

           <label htmlFor="breed">Breed</label>

           <input

             type="text"

             className="form-control"

             id="breed"

             required

             value={currentPet.breed}

             onChange={this.onChangeBreed}

             name="breed"

           />

         </div>

         <div className="form-group">

           <label htmlFor="location">Location</label>

           <input

             type="text"

             className="form-control"

             id="location"

             required

             value={currentPet.location}

             onChange={this.onChangeLocation}

             name="location"

           />

         </div>

         <div className="form-group">

           <label htmlFor="age">Age</label>

           <input

             type="text"

             className="form-control"

             id="age"

             required

             value={currentPet.age}

             onChange={this.onChangeAge}

             name="age"

           />

         </div>

         <div className="form-group">

           <label htmlFor="sex">Sex</label>

           <input

             type="text"

             className="form-control"

             id="sex"

             required

             value={currentPet.sex}

             onChange={this.onChangeSex}

             name="sex"

           />

         </div>

         <button onClick={this.savePet} className="btn btn-success">

           Submit

         </button>

       </div>

     </div>

   );

 }

}

export default connect(null, { updatePet })(EditPet);

```

You can now run the application by pointing the API calls to your local instance of Strapi. To run both the Strapi development server and your new React app, run the following:

```

# Start Strapi

npm run develop

# Start React

npm run start

```

Now Strapi will be running on port `1337`, and the React app will be running on port `3000`.

If you visit [http://localhost:3000/](http://localhost:3000/), you should see the app running:

![CRUD app running on Strapi and React](https://i.imgur.com/cdjfZAx.png)

## Conclusion

In this article, you saw how to use [Strapi, a headless CMS](https://strapi.io/), to serve as the backend for a typical CRUD application. Then, you used React and Redux to build a frontend with managed state so that changes can be propagated throughout the application.

Headless CMSes are versatile tools that can be used as part of almost any application's architecture. You can store and administer information to be consumed from different devices, platforms, and services. You can use this pattern to store content for your blog, manage products in an e-commerce platform, or build a pet adoption platform like you've seen today.