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:
In this article, I will show you how to create a Pet Adoption CRUD application. You will use a headless CMS, Strapi, for the backend, and React with Context Api. 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.
Before you can follow content properly, you need to have a basic understanding of the following.
CRUD stands for Create, Read, Update, and Delete. CRUD applications are typically composed of pages or endpoints. Most applications deployed to the internet are, at least, partially CRUD applications, and many are exclusively CRUD apps.
The image below resembles an application you will build in this article:
It has one Pet
entity listed, a “Bird”, with details about that bird. You will be able to execute CRUD operations on that entity, such as:
After that, you simply click ADD PET ENTRY button and that’s it! You have successfully created a pet entry.
Read:
For example, the display shown under “Create” is just a loop in action displaying pet data except in a nice looking way.
Now, once you click that icon button you will be redirected to an Edit Page where you will re-enter pet details with alterations.
Delete:
Headover to the next phase to first create a Strapi backend for your application.
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@latest 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-Type 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 Create new entry.
New entries are saved as "drafts" by default, so to see the pet you just added, you need to publish it.
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](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.
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.
For the Pet List, Add Pet, Update Pet, and Delete Pet features from the application, you will use React with a Context API. A Context API is an easy to integrate state management solution, in-built to React. You do not need any third party tools using the Context API.
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.
In addition to the Context API, you will also use an HTTP client library, Axios. This library use is to fetch data from the backend with the help of a readily available Strapi REST API.
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 @mui/material @emotion/react @emotion/styled @mui/icons-material axios
axios
connects to the Strapi REST API.@mui/material
a React frontend UI libraryAlright then, now that you have the above packages, move on to the next step to create an Axios base instance.
There are many ways to set up Axios in a React application. In this tutorial, we are going to use the “Base Instance” approach.
Inside the src
folder, create a separate helper http.js
file, with code that will be used to interface with the Strapi REST API.
To set up an instance of Axios (Base Instance), you have to define two things:
URL
(required) - in this context, http://localhost:1337/
.1 import axios from 'axios';
2
3 export default axios.create({
4 baseURL: "http://localhost:1337/",
5 headers: {
6 "Content-type": "application/json",
7 },
8 });
Leave the instance file for now. You will import it later in our Pet Context for making HTTP requests.
Now, you need to create a store for all the data and functions for your application. To do that, create a file and name it PetContext.js
in the directory: src/contexts/PetContext.js
.
Since this file is going to make use of the Context API, the steps below will show you how to make use of the Context API to create a Pet Context.
There are three steps to create and implement a Context API in React:
Step 1: Create the Context
In this step, you are going to create a Context, PetContext
.
Typically, in a React app you share data from one component from one component to another via prop drilling. Prop drilling, is the passing of data from one parent component to a child component via props. This is, without a doubt, limiting since you cannot share data to a component outside the parent-child branch.
Now, with the help of the Context API, you can create a Context in your App. This Context will help you share your in-app data globally irregardless of the tree structure in your React app.
In your file, PetContext.js
, import createContext
from 'react'
.
Now, create a Context like in the code below:
1 import React, { createContext } from 'react';
2
3 // create Pet Context
4 const PetContext = createContext();
Great!
Now, move on to the next step and create a provider for our newly created Pet Context.
Step 2: A Context Provider for the Pet Context
According to React, each Context you create must have a Provider. This provider is the one which takes values from your Context, and pass them to each component connected to your provider.
Create a Context Provider, PetProvider
, and pass it an empty object (empty for now at-least) value as shown below:
1 import React, { createContext } from 'react';
2
3 // create Pet Context
4 const PetContext = createContext({children});
5 // create Pet Provider
6 export const PetProvider = () => {
7 const value = {};
8 return(
9 <PetContext.Provider value={value}>
10 {children}
11 </PetContext.Provider>
12 )
13 };
Lastly, you need consume any data you will pass down via the the provider to components connected to it. Headover to the next step to enable that.
Step 3: Connecting the Pet Context to Your Root App Component
Inorder to receive and use data from your Pet Context, you need to wrap or connect the PetProvider
to a React root component, <App/>
. This allows all the components in your app to have access to all the data they need from the Pet Context.
Navigate to your index.js
file. Import PetProvider
from PetContext.js
and wrap it around the <App/>
component:
1 import React from 'react';
2 import ReactDOM from 'react-dom/client';
3 import './index.css';
4 import App from './App';
5
6 // contexts
7 import { PetProvider } from './contexts/PetContext';
8
9 const root = ReactDOM.createRoot(document.getElementById('root'));
10 root.render(
11 <React.StrictMode>
12 <PetProvider>
13 <App />
14 </PetProvider>
15 </React.StrictMode>
16 );
Congrats! You have successfully created a Pet Context for your application.
All you have to do now is to add data to your Pet Context. In your PetContext.js
file paste the following code:
1 import React, { createContext, useContext, useEffect, useState } from 'react';
2 import http from '../http';
3 const PetContext = createContext();
4
5 export const usePetContext = () => {
6 return useContext(PetContext);
7 };
8
9 export const PetProvider = ({children}) => {
10 const [pets, setPets] = useState("");
11 const [nav_value, set_nav_value] = useState("PetList");
12 const [petId, setPetId] = useState("");
13
14 // add new pet
15 const createNewPet = async (data) => {
16 await http.post("/api/pets", data);
17 };
18 // update a pet entry
19 const updatePet = async (petId, data) => {
20 await http.put(`/api/pets/${petId}`, data);
21 };
22 // delete a pet entry
23 const deletePet = async (petId) => {
24 await http.delete(`/api/pets/${petId}`);
25 };
26 // change navigation value
27 const changeNavValue = (value) => {
28 set_nav_value(value);
29 };
30 // get pet id value
31 const getPetId = (id) => {
32 setPetId(id);
33 };
34
35 useEffect(()=>{
36 const readAllPets = async () => {
37 const response = await http.get("/api/pets");
38 const responseArr = Object.values(response.data.data);
39 setPets(responseArr);
40 };
41 return readAllPets;
42 }, []);
43
44 const value = {
45 createNewPet,
46 pets,
47 updatePet,
48 deletePet,
49 changeNavValue,
50 nav_value,
51 getPetId,
52 petId
53 };
54
55 // context provider
56 return(
57 <PetContext.Provider value={value}>
58 {children}
59 </PetContext.Provider>
60 )
61 };
Done?
Awesome, now for the final part create the following components in src/components/
:
BottomNav.js
- for in-app navigation.CreatePetEntry.js
- a page with a form to add a new pet.EditPetEntry.js
- a page for editing an already existing pet entry.PetList.js
- page with a list of all pet data.PetListItem.js
- a template component for displaying a single pet entry item.Interface.js
- a component for rendering all the components.Create a component for navigating to different parts of the app and name it BottomNav.js
Code for BottomNav.js
component:
1 import * as React from 'react';
2
3 // core components
4 import BottomNavigation from '@mui/material/BottomNavigation';
5 import BottomNavigationAction from '@mui/material/BottomNavigationAction';
6
7 // icons
8 import {
9 PetsOutlined,
10 AddCircleOutline,
11 } from '@mui/icons-material';
12
13 // contexts
14 import { usePetContext } from '../contexts/PetContext';
15
16 export default function LabelBottomNavigation() {
17 const { nav_value, changeNavValue } = usePetContext();
18 const handleChange = (event, newValue) => {
19 changeNavValue(newValue);
20 };
21 return (
22 <BottomNavigation showLabels value={nav_value} onChange={handleChange}>
23 <BottomNavigationAction
24 label="Pets"
25 value="PetList"
26 icon={<PetsOutlined />}
27 />
28 <BottomNavigationAction
29 label="Add Pet"
30 value="AddPet"
31 icon={<AddCircleOutline />}
32 />
33 </BottomNavigation>
34 );
35 };
Great!
Now, create PetListItem.js
:
1 import React, { useState } from 'react';
2
3 // mui components
4 import List from '@mui/material/List';
5 import ListItemButton from '@mui/material/ListItemButton';
6 import ListItemIcon from '@mui/material/ListItemIcon';
7 import ListItemText from '@mui/material/ListItemText';
8 import Collapse from '@mui/material/Collapse';
9
10 // mui icons
11 import { IconButton, ListItem } from '@mui/material';
12 import {
13 DeleteOutline,
14 Edit,
15 ExpandMore,
16 ExpandLess,
17 LabelImportantOutlined,
18 } from '@mui/icons-material';
19
20 // nav
21 import { usePetContext } from '../contexts/PetContext';
22 export default function PetListItem({ petType, id, petFieldData}) {
23 const [open, setOpen] = useState(true);
24 const { deletePet, changeNavValue, getPetId } = usePetContext();
25 const handleClick = () => {
26 setOpen(!open);
27 };
28 const handleEditButton = () => {
29 getPetId(id);
30 changeNavValue("EditPet");
31 };
32 return (
33 <List
34 sx={{ width: '100%', bgcolor: 'background.paper' }}
35 >
36 <ListItem
37 secondaryAction={
38 <>
39 <IconButton onClick={handleEditButton} edge="end" aria-label="edit">
40 <Edit sx={{ color: 'green' }}/>
41 </IconButton>
42 <IconButton onClick={()=>deletePet(id)} edge="end" aria-label="delete" sx={{ padding: 2}}>
43 <DeleteOutline color="secondary"/>
44 </IconButton>
45 </>
46 }
47 >
48 <ListItemButton disableRipple onClick={handleClick}>
49 <ListItemIcon>
50 <LabelImportantOutlined />
51 </ListItemIcon>
52 <ListItemText
53 primary={petType}
54 secondary="Name, Breed, Location, Age, Sex"
55 />
56 {open ? <ExpandLess /> : <ExpandMore />}
57 </ListItemButton>
58 </ListItem>
59 <Collapse in={open} timeout="auto" unmountOnExit>
60 <List component="div" disablePadding>
61 {
62 petFieldData.map((item, i)=>(
63 <ListItemButton key={i} disableRipple sx={{ pl: 9 }}>
64 <ListItemIcon>
65 {item.icon}
66 </ListItemIcon>
67 <ListItemText primary={item.attrib} />
68 </ListItemButton>
69 ))
70 }
71 </List>
72 </Collapse>
73 </List>
74 );
75 };
Create PetList.js
:
1 import * as React from 'react';
2
3 // mui components
4 import Box from '@mui/material/Box';
5 import CssBaseline from '@mui/material/CssBaseline';
6 import List from '@mui/material/List';
7 import Paper from '@mui/material/Paper';
8
9 // custom components
10 import BottomNav from './BottomNav';
11 import PetListItem from './PetListItem';
12
13 // data
14 import { usePetContext } from '../contexts/PetContext';
15
16 // icons
17 import {
18 PersonOutline,
19 PetsOutlined,
20 LocationOn,
21 PunchClockOutlined,
22 TransgenderOutlined,
23 } from '@mui/icons-material';
24
25 export default function PetList() {
26 const { pets } = usePetContext();
27 return (
28 <Box sx={{ pb: 7 }}>
29 <CssBaseline />
30 <List>
31 {
32 pets && pets.map(
33 ({id, attributes: {name, animal, breed, location, age, sex}}, i)=>(
34 <PetListItem
35 key={i}
36 id={id}
37 petType={animal}
38 petFieldData={[
39 {icon: <PersonOutline/>, attrib: name},
40 {icon: <PetsOutlined/>, attrib: breed},
41 {icon: <LocationOn/>, attrib: location},
42 {icon: <PunchClockOutlined/>, attrib: age},
43 {icon: <TransgenderOutlined/>, attrib: sex}
44 ]}
45 />
46 ))
47 }
48 </List>
49 <Paper sx={{ position: 'fixed', bottom: 0, left: 0, right: 0 }} elevation={3}>
50 <BottomNav/>
51 </Paper>
52 </Box>
53 );
54 };
Create EditPetEntry.js
:
1 import React, { useState, useEffect } from 'react';
2
3 // mui components
4 import {
5 Typography,
6 TextField,
7 Box,
8 Button,
9 Paper
10 } from '@mui/material';
11
12 // mui icons
13 import { Edit } from '@mui/icons-material';
14
15 // custom components
16 import BottomNav from './BottomNav';
17
18 //axios
19 import { usePetContext } from '../contexts/PetContext';
20 export default function EditPetEntry() {
21 // input data
22 const [name, setName] = useState("");
23 const [animal, setAnimal] = useState("");
24 const [breed, setBreed] = useState("");
25 const [age, setAge] = useState("");
26 const [location, setLocation] = useState("");
27 const [sex, setSex] = useState("");
28 // edit req
29 const { updatePet, petId } = usePetContext();
30 const data = JSON.stringify({
31 "data": {
32 "name": name,
33 "animal": animal,
34 "breed": breed,
35 "age": age,
36 "location": location,
37 "sex": sex
38 }
39 });
40 const handleEditPet = () => {
41 updatePet(petId, data);
42 };
43 return (
44 <Box
45 component="form"
46 sx={{
47 '& .MuiTextField-root': { m: 1, width: '50ch' },
48 display: 'flex',
49 flexDirection: 'column'
50 }}
51 noValidate
52 autoComplete="off"
53 >
54 <div>
55 <Typography variant="h3" gutterBottom component="div">
56 Edit Pet entry
57 </Typography>
58 <TextField
59 required
60 id="filled-name"
61 label="Name"
62 variant="outlined"
63 onChange={(e)=>setName(e.target.value)}
64 />
65 <TextField
66 required
67 id="filled-animal"
68 label="Animal"
69 variant="outlined"
70 helperText="Cat, Dog, Bird"
71 onChange={(e)=>setAnimal(e.target.value)}
72 />
73 <TextField
74 required
75 id="filled-breed-input"
76 label="Breed"
77 variant="outlined"
78 onChange={(e)=>setBreed(e.target.value)}
79 />
80 <TextField
81 required
82 id="filled-location-input"
83 label="Location"
84 variant="outlined"
85 onChange={(e)=>setLocation(e.target.value)}
86 />
87 <TextField
88 required
89 id="filled-age"
90 label="Age"
91 type="number"
92 variant="outlined"
93 onChange={(e)=>setAge(e.target.value)}
94 />
95 <TextField
96 required
97 id="sex"
98 label="Sex"
99 helperText="Male, Female"
100 variant="outlined"
101 onChange={(e)=>setSex(e.target.value)}
102 />
103 </div>
104 <div>
105 <Button variant="outlined" onClick={handleEditPet} startIcon={<Edit />}>
106 Edit Pet Entry
107 </Button>
108 </div>
109 <Paper sx={{ position: 'fixed', bottom: 0, left: 0, right: 0 }} elevation={3}>
110 <BottomNav/>
111 </Paper>
112 </Box>
113 );
114 }
Create CreatePetEntry.js
:
1 import React, { useState } from 'react';
2
3 // mui components
4 import {
5 Typography,
6 TextField,
7 Box,
8 Button,
9 Paper
10 } from '@mui/material';
11
12 // icons components
13 import { Add } from '@mui/icons-material';
14
15 // custom components
16 import BottomNav from './BottomNav';
17 import { usePetContext } from '../contexts/PetContext';
18 export default function CreatePetEntry() {
19 // input data
20 const [name, setName] = useState("");
21 const [animal, setAnimal] = useState("");
22 const [breed, setBreed] = useState("");
23 const [age, setAge] = useState("");
24 const [location, setLocation] = useState("");
25 const [sex, setSex] = useState("");
26 // axios
27 const { createNewPet } = usePetContext();
28 const data = JSON.stringify({
29 "data": {
30 "name": name,
31 "animal": animal,
32 "breed": breed,
33 "age": age,
34 "location": location,
35 "sex": sex
36 }
37 })
38 const handleCreateNewPet = () => {
39 createNewPet(data);
40 };
41 return (
42 <Box
43 component="form"
44 sx={{
45 '& .MuiTextField-root': { m: 1, width: '50ch' },
46 display: 'flex',
47 flexDirection: 'column'
48 }}
49 noValidate
50 autoComplete="off"
51 >
52 <div>
53 <Typography variant="h3" gutterBottom component="div">
54 Add new Pet entry
55 </Typography>
56 <TextField
57 required
58 id="filled-name"
59 label="Name"
60 variant="filled"
61 onChange={(e)=>setName(e.target.value)}
62 />
63 <TextField
64 required
65 id="filled-animal"
66 label="Animal"
67 variant="filled"
68 helperText="Cat, Dog, Bird"
69 onChange={(e)=>setAnimal(e.target.value)}
70 />
71 <TextField
72 required
73 id="filled-breed-input"
74 label="Breed"
75 variant="filled"
76 onChange={(e)=>setBreed(e.target.value)}
77 />
78 <TextField
79 required
80 id="filled-location-input"
81 label="Location"
82 variant="filled"
83 onChange={(e)=>setLocation(e.target.value)}
84 />
85 <TextField
86 required
87 id="filled-age"
88 label="Age"
89 type="number"
90 variant="filled"
91 onChange={(e)=>setAge(e.target.value)}
92 />
93 <TextField
94 required
95 id="sex"
96 label="Sex"
97 helperText="Male, Female"
98 variant="filled"
99 onChange={(e)=>setSex(e.target.value)}
100 />
101 </div>
102 <div>
103 <Button onClick={handleCreateNewPet} variant="outlined" startIcon={<Add />}>
104 Add Pet Entry
105 </Button>
106 </div>
107 <Paper sx={{ position: 'fixed', bottom: 0, left: 0, right: 0 }} elevation={3}>
108 <BottomNav/>
109 </Paper>
110 </Box>
111 );
112 }
Create Interface.js
:
1 import React from 'react';
2
3 // custom component
4 import PetList from '../components/PetList';
5 import CreatePetEntry from '../components/CreatePetEntry';
6 import EditPetEntry from '../components/EditPetEntry';
7
8 // contexts
9 import { usePetContext } from '../contexts/PetContext';
10 const Interface = () => {
11 const { nav_value } = usePetContext();
12
13 switch (nav_value) {
14 case "PetList":
15 return <PetList/>
16 case "AddPet":
17 return <CreatePetEntry/>
18 case "EditPet":
19 return <EditPetEntry/>
20 default:
21 return <PetList/>
22 };
23 };
24 export default Interface;
Now, in your <App.js/>
file import and render the <Interface.js/>
component:
1 import './App.css';
2 import Interface from './main/Interface';
3
4 function App() {
5 return (
6 <div className="App">
7 <Interface/>
8 </div>
9 );
10 }
11 export default App;
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.
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 Context API to build a frontend with the 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.
To access the code for this article, check this GitHub repository.
Student Developer ~ Yet Another Open Source Guy ~ JavaScript/TypeScript Developer & a Tech Outlaw...