This website uses cookies

Our website, platform and/or any sub domains use cookies to understand how you use our services, and to improve both your experience and our marketing relevance.

Go hands-off. Autoscale your WordPress site on Cloudways Autonomous.Start Your Free Trial

Node.js Headless CMS: What It Is and How to Build One

Updated on July 21, 2026

12 Min Read
nodejs headless cms

Key Takeaways

  • A headless CMS stores content separately from the frontend, and Node.js lets you decide exactly how that content gets fetched and displayed.
  • Popular Node.js-friendly headless CMS options include self-hosted platforms like Strapi and Payload, and hosted ones like Contentful and Sanity.
  • This guide builds a working blog backend with Express, EJS, and Contentful, then tests it locally.
  • The finished project deploys straight from GitHub using Cloudways’ Managed Node.js Hosting.

In a headless CMS, content is stored and managed separately from the frontend. A lot of modern blogs and eCommerce sites work this way. Content lives inside the CMS and gets sent to whatever’s displaying it through an API.

Node.js pairs naturally with a headless setup because it doesn’t come with any rendering rules baked in. You decide how the content gets pulled and shown, whether that’s a server-rendered page, a JSON API for a mobile app, or anything else you want to build on top of it.

In this blog, I’ll cover why pairing Node.js with a headless CMS makes sense, go over some of the popular CMS options out there, and then build a small project to show it in practice. Lastly, I’ll deploy the whole thing on a live Cloudways server.

Why Use Node.js with a Headless CMS?

The biggest reason I’d reach for plain Node.js here is control. There’s no framework dictating how content has to be fetched or rendered, you write the logic yourself, exactly the way your project needs it.

Because Node.js isn’t tied to any particular frontend approach, the same backend can serve content however you want. You could render full HTML pages on the server, expose the content as a clean JSON API for a separate frontend or mobile app, or do both from a single codebase.

It’s also lightweight. You’re not pulling in an entire frontend framework and its build tooling just to display a list of blog posts, a small Express server and a templating engine are enough to get a working, production-ready result.

And like any headless setup, there’s the flexibility of keeping things separated. The CMS handles content creation and storage. Your Node.js backend handles logic, formatting, and delivery. Neither side gets in the other’s way.

Before jumping into the build, it’s worth knowing what’s actually out there, since “headless CMS” covers a pretty wide range of tools.

  • Strapi — Open source and self-hosted, meaning the entire CMS runs as your own Node.js app. Comes with a full admin panel out of the box, plus a plugin ecosystem if you need to extend it later.
  • Payload — TypeScript-native and code-first, you define your schema directly in code. Built with a strong focus on React and Next.js workflows, though it works fine outside that too.
  • KeystoneJS — Generates a GraphQL API automatically just from your schema definition. Minimal boilerplate if GraphQL is already how you like to query data.
  • Contentful — Fully hosted, so there’s no server to manage on your end. Comes with ready-made templates, including the Blog template I’ll be using in the mini project below.
  • Sanity — Also hosted, with real-time collaborative editing built in. Supports both its own query language (GROQ) and GraphQL, depending on preference.

In the mini project I’ll cover next, I’ll use Contentful, mainly to keep things focused on the Node.js side of the build, rather than spending time setting up and configuring a whole CMS from scratch.

How to Build a Headless Blog Using Node.js

To show you how this works in practice, I’ll build a mini project, a simple blog backend that fetches articles from a CMS and renders them as clean HTML pages using Express.

What I’ll Be Using

  • Node.js
  • VS Code
  • Contentful (Free tier)
  • Express and EJS

Step 1: Setting Up the Headless CMS (Contentful)

Before writing any backend code, I need somewhere to actually store the blog content. I’ll use the free tier of Contentful for this.

When I sign up, Contentful gives me the option to start from a template, and I’ll go with the Blog template so I’m not building out a content structure from scratch.

To confirm everything’s set up the way I expect, I’ll head over to the Content model tab and check what’s already there.

The one I actually need is page – Blog post, which comes with fields like title, SEO metadata, and the main content body.

Contentful content model showing the page - Blog post content type fields

Next, I’ll switch over to the Content tab. Since I used the template, Contentful should’ve already seeded a few sample blog posts for me. I just need to make sure their status shows as Published, since that’s what makes them fetchable through the API.

Contentful Content tab listing sample blog posts marked as published

Everything’s showing as published, so that part’s done.

Last thing before I leave Contentful: I need my API keys so my Node.js app can actually talk to it. I’ll go to Settings > API keys, click Add API key, give it a name so I remember what it’s for later, and generate the keys.

Contentful API keys settings screen for generating a Content Delivery API token

I’ll leave this tab open since I’ll need to copy the Space ID and the Content Delivery API access token in a minute.

Step 2: Setting Up the Node.js Project

Now for the backend itself. Before I can do anything, I need Node.js available on my machine.

I’m working on my office laptop, which has IT restrictions, so like usual, I’ll grab the standalone binary version of Node.js from nodejs.org instead of running a normal installer.

Node.js download page for the standalone Windows binary

Once it’s downloaded, I’ll unzip it into my Downloads folder.

Unzipped Node.js binary files inside the Downloads folder

Opening the project folder in Command Prompt

I’ll create a folder for this project on my desktop and just call it headless-blog.

Then I’ll open Command Prompt and move into that folder:

cd C:\Users\abdulrehman\Desktop\headless-blog

Command Prompt navigating into the headless-blog project folder

Pointing Command Prompt to the Node binary

Since Command Prompt has no idea where Node actually lives on my machine, I’ll point it there manually:

set PATH=%PATH%;C:\Users\abdulrehman\Downloads\node-v24.18.0-win-x64\node-v24.18.0-win-x64

Command Prompt with the PATH variable pointed to the Node.js binary

To make sure it’s actually working, I’ll run:

node -v
npm -v

Both commands return version numbers, so I’m good to continue.

Command Prompt output showing Node.js and npm version numbers

Creating the Node.js project

I’ll initialize a plain Node.js project first:

npm init -y

Command Prompt output after running npm init -y

Then I’ll install everything this project actually needs: Express to handle routing, EJS to render HTML, the official Contentful SDK to pull content, a rich-text renderer to convert Contentful’s content format into HTML, and dotenv to manage my API keys locally.

npm install express ejs contentful @contentful/rich-text-html-renderer dotenv

Command Prompt output after installing Express, EJS, Contentful, and dotenv

Step 3: Fetching Data and Building a Clean UI

Now I’ll open my project folder in VS Code. To do this, I’ll go to File > Open Folder and select my headless-blog folder.

VS Code File menu open to select the headless-blog project folder

Alright, my project’s opened in VS Code, so now it’s time to actually start writing the backend logic.

Securing the API Keys

First thing I want to do is get the Contentful keys into the project somewhere safe. To do this, in the root of my project folder, I’ll create a new file called .env and paste in:

CONTENTFUL_SPACE_ID=your_space_id_here
CONTENTFUL_ACCESS_TOKEN=your_content_delivery_api_access_token_here

VS Code .env file with Contentful space ID and access token

Now, since these are my real keys, I don’t want them in my public GitHub repo for everyone to access. To prevent this, I’ll create one more file in my project root called .gitignore, and paste in:

node_modules
.env

This tells Git to skip both whenever I push my project. node_modules because it’s just downloaded packages, no reason to upload those. And .env because that’s the file holding my actual Contentful keys, so leaving it out means they never end up somewhere public.

Setting up the Contentful Client

Next up, I need something that actually connects my app to Contentful. To do this, I’ll create a lib folder in the project root, and inside it, a file called contentful.js.

require('dotenv').config();
const contentful = require('contentful');
const client = contentful.createClient({
 space: process.env.CONTENTFUL_SPACE_ID,
 accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
 });
module.exports = { client };

VS Code contentful.js file setting up the Contentful client connection

That’s really all this file does, it just sets up the connection once, so I can import it wherever I actually need to pull data.

Building the Blog UI

This is where I’ll build the actual server: it connects to Contentful, fetches the blog posts, and passes them to a page template to display.

I’ll create a file called server.js in my project root, and paste this in:

const express = require('express');
const path = require('path');
const { client } = require('./lib/contentful');
const { documentToHtmlString } = require('@contentful/rich-text-html-renderer');

const app = express();
const PORT = process.env.PORT || 3000;

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(express.static(path.join(__dirname, 'public')));

app.get('/', async (req, res) => {
  try {
    const response = await client.getEntries({ content_type: 'pageBlogPost' });

    const posts = response.items.map((post) => ({
      title: post.fields.title,
      contentHtml: post.fields.content
        ? documentToHtmlString(post.fields.content)
        : null,
    }));

    res.render('index', { posts });
  } catch (err) {
    console.error(err);
    res.status(500).send('Something went wrong fetching posts.');
  }
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

This file connects to Contentful, fetches every post under the pageBlogPost content type ID (found the same way as before, inside Content model, opening page – Blog post, and copying the ID), and sends that data to a template called index to turn into HTML. That template doesn’t exist yet, so I’ll build it next.

I’ll create a folder called views in my project root, and inside it, a file called index.ejs. I’ll paste this in:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>My Headless CMS Blog</title>
  <link rel="stylesheet" href="/style.css">
</head>
<body>
  <main>
    <h1>My Headless CMS Blog</h1>
    <div class="posts">
      <% posts.forEach(post => { %>
        <article class="post-card">
          <h2><%= post.title %></h2>
          <% if (post.contentHtml) { %>
            <div class="post-content"><%- post.contentHtml %></div>
          <% } %>
        </article>
      <% }) %>
    </div>
  </main>
</body>
</html>

VS Code index.ejs template rendering blog post titles and content

This is the actual page the browser shows. It loops through every post server.js sends over, printing the title, and the content underneath it if there is any.

I’ll create one more folder called public, and inside it, a file called style.css. I’ll paste this in:

body {
  font-family: sans-serif;
  background-color: #f9fafb;
  margin: 0;
  padding: 40px 20px;
}

main {
  max-width: 700px;
  margin: 0 auto;
}

h1 {
  text-align: center;
  margin-bottom: 30px;
}

.post-card {
  background: #ffffff;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 20px;
  margin-bottom: 20px;
}

.post-card h2 {
  margin-top: 0;
}

VS Code style.css file with basic styling for the blog cards

This is just enough styling to keep the page from looking like plain, unstyled text once I load it in the browser.

Testing What We Built

Time to see if any of this actually works. I’ll add a start script into package.json so I’m not typing out the full node command every time:

"scripts": {
 "start": "node server.js"
 }

package.json file with the npm start script added

Then, back in Command Prompt:

npm start

And I’ll pull up http://localhost:3000 in the browser to see what happens.

And there they are, the same sample posts from the Contentful template, showing up right on the page.

Local browser preview of the headless blog running on localhost 3000

This confirms the connection is working. Contentful sent back the post data, and the Express server rendered it into HTML on the page.

Step 4: Pushing the Project to GitHub

Before deploying anywhere, I want this pushed to GitHub first.

I’ll go create a new repository there.

Then, back in Command Prompt, I’ll run through these one by one:

git init
git add .
git commit -m "First commit"
git branch -M main
git remote add origin https://github.com/abdulrehman293/nodejs-headless-cms
git push -u origin main

I’ll head back over to the repo and give it a refresh, and yep, everything’s there, minus the .env file and node_modules, which is exactly how it should be.

GitHub repository showing the pushed headless-blog project files

Step 5: Connecting Cloudways to GitHub

At this point, my project is working on the local setup and it’s also pushed to GitHub. Now I’ll move it from local to a live server. For this, I’ll use Cloudways’ Managed Node.js Hosting, built specifically for deploying projects like this straight from a Git repo.

Inside the dashboard, I’ll click Node.js from the left-side menu, then hit Launch Now.

Cloudways dashboard Node.js hosting launch screen

From there, I’ll pick a plan. Nothing about this project needs much horsepower, so the Starter plan covers it fine.

Cloudways Starter plan selection for Node.js hosting

That drops me onto the Deploy Your Node.js Web App screen. I’ll click Connect Via Git, sign into GitHub when it asks, and once Cloudways Deploy is authorized, I’ll pick my nextjs-headless-cms repo from the list and keep going.

Cloudways Connect Via Git screen with the GitHub repository selected

Step 6: Deploying the App and Going Live

This next part is where I tell Cloudways what this project is and how to run it.

I’ll set Framework Preset to Express, since that’s exactly what this backend is. For Node Version, Node 24 (LTS) is the right call here, it’s the current active long-term support line. Root Directory stays default.

Cloudways deployment settings for framework preset and Node version

Next, I’ll click Change under Build and output settings to expand it, then set Package Manager to npm, matching what I used on my machine. For Entry File, that’s server.js, same file that kicks everything off locally too.

Cloudways build and output settings with package manager and entry file

Now, since my .env file intentionally never made it into GitHub, I need to add those Contentful keys back in manually here. I’ll head to Environment Variables, hit Add, and drop in both CONTENTFUL_SPACE_ID and CONTENTFUL_ACCESS_TOKEN, same values as my local file.

Cloudways environment variables screen for adding Contentful API keys
Cloudways environment variables list with Contentful keys added

I can also import the .env file from my project folder on my computer if I don’t feel like pasting the keys manually.

Once that’s filled in, I’ll hit Deploy Now.

From here, Cloudways pulls the code straight from GitHub, installs the dependencies, and starts the app using the entry file I pointed it to. No build step involved, since this project doesn’t need one.

Cloudways deployment progress pulling code from GitHub

Moment of Truth

Once the deployment shows as successful, I’ll pull up the temporary Cloudways URL it generated.

Cloudways deployment success screen showing the temporary live URL
Headless CMS blog live on the Cloudways URL showing sample posts

And there it is. Same sample blog posts from Contentful, now being served live from Cloudways instead of sitting on my laptop. Content’s making the full trip, from Contentful, through my Express server, out to an actual public URL anyone can visit.

Live headless CMS blog displaying Contentful blog posts

Wrapping Up

So that’s the whole run-through of building a headless CMS setup with Node.js. I covered why plain Node.js is a solid choice for this, went over the popular CMS options out there, both self-hosted ones like Strapi and hosted ones like Contentful, and used Contentful for the actual build here.

I also walked through putting together a small Express backend that pulls articles from a CMS, renders them out with EJS, tested it locally, and then took it live on Cloudways.

I’ve pushed the finished project to my GitHub, so feel free to clone it and reuse the code. And if you want to deploy something similar yourself, our Managed Node.js Hosting handles this exact workflow: connect your repo, pick your framework preset, and you’re live.

Q. Is Node.js a CMS?

Nope. Node.js is just a JavaScript runtime, not a content management system on its own. That said, some popular headless CMS platforms, like Strapi, KeystoneJS, and Payload, are actually built with Node.js. Others, like Contentful and Sanity, are hosted separately and just get consumed by a Node.js app through an API.

Q. What is the best headless CMS for Node.js?

Depends on what you’re after. If you want to self-host and own everything end to end, Strapi’s a strong pick, and it’s built with Node.js itself. If you’d rather skip hosting a CMS altogether and just pull content through an API, Contentful or Sanity get you there faster.

Q. Does a headless CMS help with SEO?

It can. The CMS stores your content, and how you render that content on the server determines how crawlable it is. Server-rendered HTML, like what this project builds with Express and EJS, is generally easy for search engines to read.

Q. Do I have to use Express with Node.js for this?

No, Express just happens to be one of the most common choices for building a Node.js backend. You could use Fastify, Koa, or even Node’s built-in http module instead, the core idea of fetching from a headless CMS and rendering the result stays the same either way.

Share your opinion in the comment section. COMMENT NOW

Share This Article

Start Growing with Cloudways Today.

Our Clients Love us because we never compromise on these

Abdul Rehman

Abdul is a tech-savvy, coffee-fueled, and creatively driven marketer who loves keeping up with the latest software updates and tech gadgets. He's also a skilled technical writer who can explain complex concepts simply for a broad audience. Abdul enjoys sharing his knowledge of the Cloud industry through user manuals, documentation, and blog posts.

×

Webinar: How to Get 100% Scores on Core Web Vitals

Join Joe Williams & Aleksandar Savkovic on 29th of March, 2021.

Do you like what you read?

Get the Latest Updates

Share Your Feedback

Please insert Content

Thank you for your feedback!

Do you like what you read?

Get the Latest Updates

Share Your Feedback

Please insert Content

Thank you for your feedback!

Want to Experience the Cloudways Platform in Its Full Glory?

Take a FREE guided tour of Cloudways and see for yourself how easily you can manage your server & apps on the leading cloud-hosting platform.

Start my tour