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
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.
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.
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.
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)
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.
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.
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.
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.
Once the plugin is installed, you can go to http://localhost:1337/graphql to view and test the endpoint.
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
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
andreact-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, 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/, you should see the app running:
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.