What is Headless CMS ?

Bayram EKER
16 min readDec 19, 2022

--

Headless content management system

Traditional CMS

A traditional CMS (often described as monolithic) couples the frontend (the design of a website and its content) and the backend (the interface used to create content) bundled into a single application that is web-first.

CMS providers like Wix, WordPress, and Squarespace are good examples of the traditional model. They usually require the use of a specific framework or language with everything tied into a specific application.

Headless CMS

A headless Content Management System, or headless CMS, is a back end-only web content management system that acts primarily as a content repository. A headless CMS makes content accessible via an API for display on any device, without a built-in front end or presentation layer. The term ‘headless’ comes from the concept of chopping the ‘head’ (the front end) off the ‘body’ (the back end).[1]

Simply put, a headless CMS is a content management system that manages and organizes content without a connected front-end or display layer. The headless CMS is where all of your content and assets live. Then, you use a content API to distribute that content to anywhere and everywhere you need it — your website, your mobile app, your email marketing, your CRM, etc.

While traditional CMS, like WordPress, serve small businesses well and still dominate the market, they present significant limitations, including:

  • Scalability: Growing your business and improving workflows will be significantly hindered by reliance on developers and technical glitches
  • Support: These open-source systems have a great community but lack the technical resources you need to grow an enterprise company
  • Security: Traditional CMS puts your company and customers at risk- they lack the infrastructure to keep your digital content Secure
  • Speed: These outdated systems are notoriously slow and hinder your ability to rank content

I like to use this diagram to illustrate how Headless works, so hopefully it paints a clearer picture.

A headless CMS vs. a traditional CMS

While a headless CMS separates content from the presentation so the same content can appear on any channel or device, a traditional CMS locks the content and presentation together. This means that a traditional CMS typically delivers content through a single channel — usually a web browser. Meanwhile, a headless CMS uses APIs to present a single set of content in multiple channels. This is why a headless CMS is sometimes known as an “API-first” CMS.

Headless components and architecture — what you need to know

A strong basis for a successful headless commerce stack comprises of a headless commerce platform, a headless CMS and a headless frontend. They are all connected via API.

The whole headless commerce architecture is based on the concept of API-first approach. Thanks to this, you are completely independent of a single provider and if necessary, you can replace your technology pretty easily.

To discuss headless architecture, let’s establish what are the three main components of each website. These are:

  • Front-end — a layer visible to the user, in which all information is transferred, the entire graphic interface
  • Back-end — the layer by which it is possible to manage the correct operation of the website
  • Database — a container that stores all kinds of data on prices, descriptions, graphics, names, etc.

Traditional (monolithic) solutions usually use a combination of all three components within the same platform. Headless commerce, on the other hand, allows the front-end to work separately and independently from the other parts.

This way you can put both parts of the application on different servers and easily separate their load. Additionally, by using Vue Storefront you can combine numerous 3-rd party headless solutions that work best for your online store. Do you feel stuck in the technology imposed by your traditional monolithic system? There’s no need for that anymore.

Thanks to the headless commerce approach, front-end and back-end specialists can update and work both quickly and independently. One team does not have to wait for the results of the other’s work.

Examples of headless CMS

Open source headless CMSs include:

  • Strapi (which we recommend from our own experience)
  • Cockpit
  • Directus

However, non-open source solutions exist. SaaS options include:

  • Core dna
  • Contentful
  • Kentico Cloud

Prerequisites

Before you can follow content properly, you need to have a basic understanding of the following.

  1. Basic knowledge of JavaScript ES6 syntax and features
  2. Basic knowledge of ReactJS terminology: JSX, State, Asynchronous JavaScript, etc.
  3. Basic understanding of Strapi — get started here.
  4. Basic understanding of Restful APIs.

Application Demo

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:

  • Create:
  • To perform a “Create” operation, i.e. to add a pet to the listing, you click on “Add Pet” button.
  • Once you click on Add Pet, you’ll be redirected to a page similar to that below:
  • Now, from the Add Pet page, you will have to fill in the pet details in each respective field.
  • After that, you simply click ADD PET ENTRY button and that’s it! You have successfully created a pet entry.
  • Read:
  • To “Read” all pet entries i.e to list all pets from the database. To execute this, in a nutshell, you need to loop through all the pet data using JavaScript.
  • For example, the display shown under “Create” is just a loop in action displaying pet data except in a nice looking way.
  • Update: — To Update a pet entry, i.e. to edit an already created pet entry, you have to click an Edit button. — From the pet list, you will see a green pencil icon, see the circled icon below;
  • The pencil icon is nothing more but an icon button, in this context, an “edit pet icon button”.
  • 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:
  • To delete a pet entry, you click on the bin icon located at the right side of the pencil icon.
  • That bin icon is the icon button for “delete pet entry”, intuitive huh?

Headover to the next phase to first create a Strapi backend for your application.

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

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](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 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 library

Alright then, now that you have the above packages, move on to the next step to create an Axios base instance.

Setting up 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:

  • a default URL (required) - in this context, http://localhost:1337/.
  • a request header — this is optional, since in this tutorial you do not have any authorization to do.

import axios from 'axios';

export default axios.create({
baseURL: "http://localhost:1337/",
headers: {
"Content-type": "application/json",
},
});

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.

Creating 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:


import React, { createContext } from 'react';

// create Pet Context
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:


import React, { createContext } from 'react';

// create Pet Context
const PetContext = createContext({children});
// create Pet Provider
export const PetProvider = () => {
const value = {};
return(
<PetContext.Provider value={value}>
{children}
</PetContext.Provider>
)
};

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:


import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

// contexts
import { PetProvider } from './contexts/PetContext';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<PetProvider>
<App />
</PetProvider>
</React.StrictMode>
);

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:


import React, { createContext, useContext, useEffect, useState } from 'react';
import http from '../http';
const PetContext = createContext();

export const usePetContext = () => {
return useContext(PetContext);
};

export const PetProvider = ({children}) => {
const [pets, setPets] = useState("");
const [nav_value, set_nav_value] = useState("PetList");
const [petId, setPetId] = useState("");

// add new pet
const createNewPet = async (data) => {
await http.post("/api/pets", data);
};
// update a pet entry
const updatePet = async (petId, data) => {
await http.put(`/api/pets/${petId}`, data);
};
// delete a pet entry
const deletePet = async (petId) => {
await http.delete(`/api/pets/${petId}`);
};
// change navigation value
const changeNavValue = (value) => {
set_nav_value(value);
};
// get pet id value
const getPetId = (id) => {
setPetId(id);
};

useEffect(()=>{
const readAllPets = async () => {
const response = await http.get("/api/pets");
const responseArr = Object.values(response.data.data);
setPets(responseArr);
};
return readAllPets;
}, []);

const value = {
createNewPet,
pets,
updatePet,
deletePet,
changeNavValue,
nav_value,
getPetId,
petId
};

// context provider
return(
<PetContext.Provider value={value}>
{children}
</PetContext.Provider>
)
};

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:


import * as React from 'react';

// core components
import BottomNavigation from '@mui/material/BottomNavigation';
import BottomNavigationAction from '@mui/material/BottomNavigationAction';

// icons
import {
PetsOutlined,
AddCircleOutline,
} from '@mui/icons-material';

// contexts
import { usePetContext } from '../contexts/PetContext';

export default function LabelBottomNavigation() {
const { nav_value, changeNavValue } = usePetContext();
const handleChange = (event, newValue) => {
changeNavValue(newValue);
};
return (
<BottomNavigation showLabels value={nav_value} onChange={handleChange}>
<BottomNavigationAction
label="Pets"
value="PetList"
icon={<PetsOutlined />}
/>
<BottomNavigationAction
label="Add Pet"
value="AddPet"
icon={<AddCircleOutline />}
/>
</BottomNavigation>
);
};

Great!

Now, create PetListItem.js:


import React, { useState } from 'react';

// mui components
import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Collapse from '@mui/material/Collapse';

// mui icons
import { IconButton, ListItem } from '@mui/material';
import {
DeleteOutline,
Edit,
ExpandMore,
ExpandLess,
LabelImportantOutlined,
} from '@mui/icons-material';

// nav
import { usePetContext } from '../contexts/PetContext';
export default function PetListItem({ petType, id, petFieldData}) {
const [open, setOpen] = useState(true);
const { deletePet, changeNavValue, getPetId } = usePetContext();
const handleClick = () => {
setOpen(!open);
};
const handleEditButton = () => {
getPetId(id);
changeNavValue("EditPet");
};
return (
<List
sx={{ width: '100%', bgcolor: 'background.paper' }}
>
<ListItem
secondaryAction={
<>
<IconButton onClick={handleEditButton} edge="end" aria-label="edit">
<Edit sx={{ color: 'green' }}/>
</IconButton>
<IconButton onClick={()=>deletePet(id)} edge="end" aria-label="delete" sx={{ padding: 2}}>
<DeleteOutline color="secondary"/>
</IconButton>
</>
}
>
<ListItemButton disableRipple onClick={handleClick}>
<ListItemIcon>
<LabelImportantOutlined />
</ListItemIcon>
<ListItemText
primary={petType}
secondary="Name, Breed, Location, Age, Sex"
/>
{open ? <ExpandLess /> : <ExpandMore />}
</ListItemButton>
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{
petFieldData.map((item, i)=>(
<ListItemButton key={i} disableRipple sx={{ pl: 9 }}>
<ListItemIcon>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.attrib} />
</ListItemButton>
))
}
</List>
</Collapse>
</List>
);
};

Create PetList.js:



import * as React from 'react';

// mui components
import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline';
import List from '@mui/material/List';
import Paper from '@mui/material/Paper';

// custom components
import BottomNav from './BottomNav';
import PetListItem from './PetListItem';

// data
import { usePetContext } from '../contexts/PetContext';

// icons
import {
PersonOutline,
PetsOutlined,
LocationOn,
PunchClockOutlined,
TransgenderOutlined,
} from '@mui/icons-material';

export default function PetList() {
const { pets } = usePetContext();
return (
<Box sx={{ pb: 7 }}>
<CssBaseline />
<List>
{
pets && pets.map(
({id, attributes: {name, animal, breed, location, age, sex}}, i)=>(
<PetListItem
key={i}
id={id}
petType={animal}
petFieldData={[
{icon: <PersonOutline/>, attrib: name},
{icon: <PetsOutlined/>, attrib: breed},
{icon: <LocationOn/>, attrib: location},
{icon: <PunchClockOutlined/>, attrib: age},
{icon: <TransgenderOutlined/>, attrib: sex}
]}
/>
))
}
</List>
<Paper sx={{ position: 'fixed', bottom: 0, left: 0, right: 0 }} elevation={3}>
<BottomNav/>
</Paper>
</Box>
);
};

Create EditPetEntry.js:


import React, { useState, useEffect } from 'react';

// mui components
import {
Typography,
TextField,
Box,
Button,
Paper
} from '@mui/material';

// mui icons
import { Edit } from '@mui/icons-material';

// custom components
import BottomNav from './BottomNav';

//axios
import { usePetContext } from '../contexts/PetContext';
export default function EditPetEntry() {
// input data
const [name, setName] = useState("");
const [animal, setAnimal] = useState("");
const [breed, setBreed] = useState("");
const [age, setAge] = useState("");
const [location, setLocation] = useState("");
const [sex, setSex] = useState("");
// edit req
const { updatePet, petId } = usePetContext();
const data = JSON.stringify({
"data": {
"name": name,
"animal": animal,
"breed": breed,
"age": age,
"location": location,
"sex": sex
}
});
const handleEditPet = () => {
updatePet(petId, data);
};
return (
<Box
component="form"
sx={{
'& .MuiTextField-root': { m: 1, width: '50ch' },
display: 'flex',
flexDirection: 'column'
}}
noValidate
autoComplete="off"
>
<div>
<Typography variant="h3" gutterBottom component="div">
Edit Pet entry
</Typography>
<TextField
required
id="filled-name"
label="Name"
variant="outlined"
onChange={(e)=>setName(e.target.value)}
/>
<TextField
required
id="filled-animal"
label="Animal"
variant="outlined"
helperText="Cat, Dog, Bird"
onChange={(e)=>setAnimal(e.target.value)}
/>
<TextField
required
id="filled-breed-input"
label="Breed"
variant="outlined"
onChange={(e)=>setBreed(e.target.value)}
/>
<TextField
required
id="filled-location-input"
label="Location"
variant="outlined"
onChange={(e)=>setLocation(e.target.value)}
/>
<TextField
required
id="filled-age"
label="Age"
type="number"
variant="outlined"
onChange={(e)=>setAge(e.target.value)}
/>
<TextField
required
id="sex"
label="Sex"
helperText="Male, Female"
variant="outlined"
onChange={(e)=>setSex(e.target.value)}
/>
</div>
<div>
<Button variant="outlined" onClick={handleEditPet} startIcon={<Edit />}>
Edit Pet Entry
</Button>
</div>
<Paper sx={{ position: 'fixed', bottom: 0, left: 0, right: 0 }} elevation={3}>
<BottomNav/>
</Paper>
</Box>
);
}

Create CreatePetEntry.js:


import React, { useState } from 'react';

// mui components
import {
Typography,
TextField,
Box,
Button,
Paper
} from '@mui/material';

// icons components
import { Add } from '@mui/icons-material';

// custom components
import BottomNav from './BottomNav';
import { usePetContext } from '../contexts/PetContext';
export default function CreatePetEntry() {
// input data
const [name, setName] = useState("");
const [animal, setAnimal] = useState("");
const [breed, setBreed] = useState("");
const [age, setAge] = useState("");
const [location, setLocation] = useState("");
const [sex, setSex] = useState("");
// axios
const { createNewPet } = usePetContext();
const data = JSON.stringify({
"data": {
"name": name,
"animal": animal,
"breed": breed,
"age": age,
"location": location,
"sex": sex
}
})
const handleCreateNewPet = () => {
createNewPet(data);
};
return (
<Box
component="form"
sx={{
'& .MuiTextField-root': { m: 1, width: '50ch' },
display: 'flex',
flexDirection: 'column'
}}
noValidate
autoComplete="off"
>
<div>
<Typography variant="h3" gutterBottom component="div">
Add new Pet entry
</Typography>
<TextField
required
id="filled-name"
label="Name"
variant="filled"
onChange={(e)=>setName(e.target.value)}
/>
<TextField
required
id="filled-animal"
label="Animal"
variant="filled"
helperText="Cat, Dog, Bird"
onChange={(e)=>setAnimal(e.target.value)}
/>
<TextField
required
id="filled-breed-input"
label="Breed"
variant="filled"
onChange={(e)=>setBreed(e.target.value)}
/>
<TextField
required
id="filled-location-input"
label="Location"
variant="filled"
onChange={(e)=>setLocation(e.target.value)}
/>
<TextField
required
id="filled-age"
label="Age"
type="number"
variant="filled"
onChange={(e)=>setAge(e.target.value)}
/>
<TextField
required
id="sex"
label="Sex"
helperText="Male, Female"
variant="filled"
onChange={(e)=>setSex(e.target.value)}
/>
</div>
<div>
<Button onClick={handleCreateNewPet} variant="outlined" startIcon={<Add />}>
Add Pet Entry
</Button>
</div>
<Paper sx={{ position: 'fixed', bottom: 0, left: 0, right: 0 }} elevation={3}>
<BottomNav/>
</Paper>
</Box>
);
}

Create Interface.js:


import React from 'react';

// custom component
import PetList from '../components/PetList';
import CreatePetEntry from '../components/CreatePetEntry';
import EditPetEntry from '../components/EditPetEntry';

// contexts
import { usePetContext } from '../contexts/PetContext';
const Interface = () => {
const { nav_value } = usePetContext();

switch (nav_value) {
case "PetList":
return <PetList/>
case "AddPet":
return <CreatePetEntry/>
case "EditPet":
return <EditPetEntry/>
default:
return <PetList/>
};
};
export default Interface;

Now, in your <App.js/> file import and render the <Interface.js/> component:


import './App.css';
import Interface from './main/Interface';

function App() {
return (
<div className="App">
<Interface/>
</div>
);
}
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.

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

--

--

Responses (1)