DEV Community

Cover image for How to Build a CRUD App with React and a Headless CMS
Shada for Strapi

Posted on • Originally published at strapi.io

How to Build a CRUD App with React and a Headless CMS

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 for the backend, and React with Redux 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, 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, 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.

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


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

Enter fullscreen mode Exit fullscreen mode

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 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.

Register Admin

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

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

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

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

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

Now you can call the 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

Using the Strapi GraphQL Plugin

If instead of using the REST API, you want to use a GraphQL 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

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

GraphQL Query

Building the Frontend

For the Pet List, Add Pet, Update Pet, and Delete Pet features from the application, you will use React with Redux. 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.

First, create a new React application:


npx create-react-app pet-adoption

Enter fullscreen mode Exit fullscreen mode

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


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

Enter fullscreen mode Exit fullscreen mode
  • 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",

 },

});

Enter fullscreen mode Exit fullscreen mode

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();

Enter fullscreen mode Exit fullscreen mode

Redux uses actions and reducers. According to the Redux documentation, 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";

Enter fullscreen mode Exit fullscreen mode

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);

 }

};

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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,

 },

});

Enter fullscreen mode Exit fullscreen mode

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")

);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

If you visit http://localhost:3000/, you should see the app running:

CRUD app running on Strapi and React

Conclusion

In this article, you saw how to use Strapi, a headless CMS, 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.

Discussion (0)