JoinDownload
This is drafted post. Please setdraft: falsein this .mdx file once ready to be published.

Create a GraphQL CMS in NodeJS

5 Min Read

Vyom Srivastava

CMS or Content Management System is a platform that lets users create, edit, delete and read the content of a website without the need for technical knowledge. It provides an easy to use UI that lets the users login and manage users as well. For example, WordPress is the most popular CMS platform. It lets you create different types of users, supports plugins, and it even lets you create an e-commerce website.

Headless CMS is a very hot topic in the web development world. To understand the Headless CMS, let us go back to the traditional CMS like WordPress, Joomla, etc where the content of the website is locked into your website and you can’t use it anywhere else. Headless CMS changes all of that. With Headless CMS, the content and the website i.e. head of the CMS are disconnected hence it allows you to use your content anywhere. It allows you to keep the content part of the CMS and uses API to deliver on any platform like website, mobile app, etc.

The best part about headless CMS is that it provides API for all of its functioning. Therefore, you just need to publish your content once and it’ll deliver it on any platform you want to. This approach lets you focus on the content part and the delivery part separately. So in case you have a team, it’ll be very easy for you to manage things.

Now we have covered a lot about the CMS as well as GraphQL (we even created a chatting app using GraphQL). So, let's try to create a Headless CMS using GraphQL.

Prerequisites

  • Basic knowledge of GraphQL
  • Basic knowledge of NodeJS

Step -1: Initializing the project

Now open your terminal, create a new folder and open it:

mkdir cmsql
cd cmsql

Now once you’re in the folder, initialize a node project:

npm init

Once the above is done, let us install the project dependencies:

npm install graphql express-jwt slugify jsonwebtoken apollo-server-express express body-parser graphql-tools dotenv mysql2 sequelize bcrypt@3.0.6

Step -2: Define the GraphQL Schema

Create a new folder data and then create a new file schema.js and paste the below code:

//File Path: data/schema.js
const { makeExecutableSchema } = require("graphql-tools")
const resolvers = require("./resolvers")
// Define our schema using the GraphQL schema language
const typeDefs = `
scalar DateTime
type User {
id: Int!
firstName: String!
lastName: String
email: String!
posts: [Post]
createdAt: DateTime! # will be generated
updatedAt: DateTime! # will be generated
}
type Post {
id: Int!
title: String!
slug: String!
content: String!
status: Boolean!
user: User!
tags: [Tag!]!
createdAt: DateTime! # will be generated
updatedAt: DateTime! # will be generated
}
type Tag {
id: Int!
name: String!
slug: String!
description: String
posts: [Post]
createdAt: DateTime! # will be generated
updatedAt: DateTime! # will be generated
}
type Query {
allUsers: [User]
fetchUser(id: Int!): User
allPosts: [Post]
fetchPost(id: Int!): Post
allTags: [Tag]
fetchTag(id: Int!): Tag
}
type Mutation {
login (
email: String!,
password: String!
): String
createUser (
firstName: String!,
lastName: String,
email: String!,
password: String!
): User
updateUser (
id: Int!,
firstName: String!,
lastName: String,
email: String!,
password: String!
): User
addPost (
title: String!,
content: String!,
status: Boolean
tags: [Int!]!
): Post
updatePost (
id: Int!,
title: String!,
content: String!,
status: Boolean,
tags: [Int!]!
): Post
deletePost (
id: Int!
): Boolean
addTag (
name: String!,
description: String
): Tag
updateTag (
id: Int!,
name: String!,
description: String
): Tag
deleteTag (
id: Int!
): Boolean
}
`
module.exports = makeExecutableSchema({ typeDefs, resolvers })

Code Explanation:

We have defined the schema here in which we have got three fields:

  • User (id, name, first name, last name, email, password, created at, updated at)
  • Post (id, title, slug, content, status, user, tag, created at, updated at)
  • Tag (id, name, slug, descriptions, posts, created at, updated at)

Later on we have defined six queries:

  • allUsers

  • fetchUser

  • allPosts

  • fetchPost

  • allTags

  • fetchTag At last, we have defined our nine mutations:

  • login

  • createUser

  • updateUser

  • addPost

  • updatePost

  • deletePost

  • addTag

  • updateTag

  • deleteTag

Step -3: Setting up Resolvers:

Now in the same folder i.e. data, create another file resolvers.js and paste the below code:

// File Path: data/resolvers.js
"use strict"
const { GraphQLScalarType } = require("graphql")
const { Kind } = require("graphql/language")
const { User, Post, Tag } = require("../models")
const bcrypt = require("bcrypt")
const jwt = require("jsonwebtoken")
const slugify = require("slugify")
require("dotenv").config()
// Define resolvers
const resolvers = {
Query: {
// Fetch all users
async allUsers() {
return await User.all()
},
// Get a user by it ID
async fetchUser(_, { id }) {
return await User.findById(id)
},
// Fetch all posts
async allPosts() {
return await Post.all()
},
// Get a post by it ID
async fetchPost(_, { id }) {
return await Post.findById(id)
},
// Fetch all tags
async allTags(_, args, { user }) {
return await Tag.all()
},
// Get a tag by it ID
async fetchTag(_, { id }) {
return await Tag.findById(id)
},
},
Mutation: {
// Handles user login
async login(_, { email, password }) {
const user = await User.findOne({ where: { email } })
if (!user) {
throw new Error("No user with that email")
}
const valid = await bcrypt.compare(password, user.password)
if (!valid) {
throw new Error("Incorrect password")
}
// Return json web token
return jwt.sign(
{
id: user.id,
email: user.email,
},
process.env.JWT_SECRET,
{ expiresIn: "1y" }
)
},
// Create new user
async createUser(_, { firstName, lastName, email, password }) {
return await User.create({
firstName,
lastName,
email,
password: await bcrypt.hash(password, 10),
})
},
// Update a particular user
async updateUser(
_,
{ id, firstName, lastName, email, password },
{ authUser }
) {
// Make sure user is logged in
if (!authUser) {
throw new Error("You must log in to continue!")
}
// fetch the user by it ID
const user = await User.findById(id)
// Update the user
await user.update({
firstName,
lastName,
email,
password: await bcrypt.hash(password, 10),
})
return user
},
// Add a new post
async addPost(_, { title, content, status, tags }, { authUser }) {
// Make sure user is logged in
if (!authUser) {
throw new Error("You must log in to continue!")
}
const user = await User.findOne({ where: { id: authUser.id } })
const post = await Post.create({
userId: user.id,
title,
slug: slugify(title, { lower: true }),
content,
status,
})
// Assign tags to post
await post.setTags(tags)
return post
},
// Update a particular post
async updatePost(
_,
{ id, title, content, status, tags },
{ authUser }
) {
// Make sure user is logged in
if (!authUser) {
throw new Error("You must log in to continue!")
}
// fetch the post by it ID
const post = await Post.findById(id)
// Update the post
await post.update({
title,
slug: slugify(title, { lower: true }),
content,
status,
})
// Assign tags to post
await post.setTags(tags)
return post
},
// Delete a specified post
async deletePost(_, { id }, { authUser }) {
// Make sure user is logged in
if (!authUser) {
throw new Error("You must log in to continue!")
}
// fetch the post by it ID
const post = await Post.findById(id)
return await post.destroy()
},
// Add a new tag
async addTag(_, { name, description }, { authUser }) {
// Make sure user is logged in
if (!authUser) {
throw new Error("You must log in to continue!")
}
return await Tag.create({
name,
slug: slugify(name, { lower: true }),
description,
})
},
// Update a particular tag
async updateTag(_, { id, name, description }, { authUser }) {
// Make sure user is logged in
if (!authUser) {
throw new Error("You must log in to continue!")
}
// fetch the tag by it ID
const tag = await Tag.findById(id)
// Update the tag
await tag.update({
name,
slug: slugify(name, { lower: true }),
description,
})
return tag
},
// Delete a specified tag
async deleteTag(_, { id }, { authUser }) {
// Make sure user is logged in
if (!authUser) {
throw new Error("You must log in to continue!")
}
// fetch the tag by it ID
const tag = await Tag.findById(id)
return await tag.destroy()
},
},
User: {
// Fetch all posts created by a user
async posts(user) {
return await user.getPosts()
},
},
Post: {
// Fetch the author of a particular post
async user(post) {
return await post.getUser()
},
// Fetch alls tags that a post belongs to
async tags(post) {
return await post.getTags()
},
},
Tag: {
// Fetch all posts belonging to a tag
async posts(tag) {
return await tag.getPosts()
},
},
DateTime: new GraphQLScalarType({
name: "DateTime",
description: "DateTime type",
parseValue(value) {
// value from the client
return new Date(value)
},
serialize(value) {
const date = new Date(value)
// value sent to the client
return date.toISOString()
},
parseLiteral(ast) {
if (ast.kind === Kind.INT) {
// ast value is always in string format
return parseInt(ast.value, 10)
}
return null
},
}),
}
module.exports = resolvers

Code Explanation:

Don’t worry about the size of the code. Let’s break it into parts then then understand what’s going on here. I am assuming that you know about resolvers, if you don’t then you can check out our article on [GraphQL basics](PUBLISHED URL HERE).

We have defined our queries and what to return when they’re called.

  • allUsers(): Will return all the user details
  • fetchUser(): Will return a user by taking the ID of that user.
  • allPosts(): Will return all the posts
  • fetchPost(): Similar to fetchUser(), it returns a post by taking the ID of that post.
  • allTags(): Will return all the tags
  • fetchTag(): Similar to fetchUser(), it returns a tag by taking the ID of that tag.

After that we have the definition of all the mutations:

  • login(): It accepts an email and password and returns the JWT token.
  • createUser(): It creates a user by accepting the email, password, first name, and last name.
  • updateUser(): It updated the details of a user who is logged in. It accepts: id, firstName, lastName, email, password, and the bearer token.
  • addPost(): It creates a new post for the user who is logged in. It accepts: title, content, status, tags and the bearer token.
  • updatePost(): It updates an existing post by accepting: id, title, content, status, tags of a post, and the bearer token of the logged-in user.
  • deletePost(): It deletes an existing post. It accepts the id of the post and the bearer token of the logged-in user.
  • addTag(): It is used to add a new tag. It accepts the name and description of the tag with the bearer token of the logged-in user.
  • updateTag(): It updates an existing tag using the id, name, and description of the tag with the bearer token of the logged-in user.
  • deleteTag(): It deletes an existing tag. It accepts the id of the post and the bearer token of the logged-in user.

Step -4: Creating the Server:

Now go back to the root folder, create a file server.js and paste the below code:

"use strict"
const express = require("express")
const bodyParser = require("body-parser")
const {
graphqlExpress,
graphiqlExpress,
} = require("apollo-server-express")
const schema = require("./data/schema")
const jwt = require("express-jwt")
require("dotenv").config()
const PORT = 3000
// Create our express app
const app = express()
// Graphql endpoint
app.use(
"/api",
bodyParser.json(),
jwt({
secret: process.env.JWT_SECRET,
credentialsRequired: false,
}),
graphqlExpress(req => ({
schema,
context: {
authUser: req.user,
},
}))
)
// Graphiql for testing the API out
app.use("/graphiql", graphiqlExpress({ endpointURL: "api" }))
app.listen(PORT, () => {
console.log(
`GraphiQL is running on http://localhost:${PORT}/graphiql`
)
})

Code Explanation:

We are importing all the required packages which will be used to run the server. We are using port 3000 here, if you want to use some other port you can use that but make sure that it isn’t occupied by some other service.

Step -5: Defining Models:

Now create a folder models in the project root and open it. Once you’re in the folder create a new file index.js and paste the below code:

// File Path: models/index.js
"use strict"
var fs = require("fs")
var path = require("path")
var Sequelize = require("sequelize")
var basename = path.basename(__filename)
var env = process.env.NODE_ENV || "development"
var config = require(__dirname + "/../config/config.js")[env]
var db = {}
if (config.use_env_variable) {
var sequelize = new Sequelize(process.env[config.use_env_variable])
} else {
var sequelize = new Sequelize(
config.database,
config.username,
config.password,
config
)
}
fs.readdirSync(__dirname)
.filter(file => {
return (
file.indexOf(".") !== 0 &&
file !== basename &&
file.slice(-3) === ".js"
)
})
.forEach(file => {
var model = sequelize["import"](path.join(__dirname, file))
db[model.name] = model
})
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db)
}
})
db.sequelize = sequelize
db.Sequelize = Sequelize
module.exports = db

Code Explanation:

We’re trying to create an object of our database connection so that we can use it anywhere we want to.

Now create another file post.js and paste the below code:

// File Path: models/post.js
"use strict"
module.exports = (sequelize, DataTypes) => {
const Post = sequelize.define("Post", {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
userId: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
},
title: {
type: DataTypes.STRING,
allowNull: false,
},
slug: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
content: {
type: DataTypes.STRING,
allowNull: false,
},
status: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
})
Post.associate = function (models) {
// A post belongs to a user
Post.belongsTo(models.User)
// A post can belong to many tags
Post.belongsToMany(models.Tag, { through: "post_tag" })
}
return Post
}

Create another file posttag.js and paste the below code:

// File path: models/posttag.js
"use strict"
module.exports = (sequelize, DataTypes) => {
const PostTag = sequelize.define(
"PostTag",
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
postId: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
},
tagId: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
},
},
{
classMethods: {
associate: function (models) {
// associations can be defined here
},
},
}
)
return PostTag
}

Create file tag.js and paste the below code:

// File Path: models/tag.js
"use strict"
module.exports = (sequelize, DataTypes) => {
const Tag = sequelize.define("Tag", {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
name: {
type: DataTypes.STRING,
unique: true,
allowNull: false,
},
slug: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
description: DataTypes.STRING,
})
Tag.associate = function (models) {
// A tag can have too many posts
Tag.belongsToMany(models.Post, { through: "post_tag" })
}
return Tag
}

Create one last file user.js and paste the below code:

"use strict"
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define("User", {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
firstName: {
type: DataTypes.STRING,
allowNull: false,
},
lastName: DataTypes.STRING,
email: {
type: DataTypes.STRING,
unique: true,
allowNull: false,
},
password: {
type: DataTypes.STRING,
allowNull: false,
},
})
User.associate = function (models) {
// A user can have many post
User.hasMany(models.Post)
}
return User
}

Code Explanation:

In all the above we have defined the tables as objects and defined the data type for all the table columns.

Step -6: Create the configuration file:

Now again go back to your project root, create a folder config and open it. Once you’re in the folder, create a file config.js and paste the below code:

"use strict"
require("dotenv").config()
module.exports = {
development: {
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOST,
dialect: "mysql",
operatorsAliases: false,
},
test: {
username: "root",
password: null,
database: "database_test",
host: "127.0.0.1",
dialect: "mysql",
operatorsAliases: false,
},
production: {
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOST,
dialect: "mysql",
operatorsAliases: false,
},
}

Code Explanation:

We have imported the dotenv package which will help us to fetch the data from the .env file and use it globally in the project. After that, we have defined three modules which will be used as production, test, and development.

Step -7: Creating .env and migrations:

Now go back to the project root, use command vi .env and paste the below code:

NODE_ENV=development
DB_HOST=localhost
DB_USERNAME=YOUR_USERNAME_OF_MYSQL
DB_PASSWORD=PASSWORD_OF_MYSQL_USER
DB_NAME=graphql_blog_cms
JWT_SECRET=ANY_SECRET_STRING

Code Explanation:

Replace the placeholders with your username, password, and a JWT secret. This is an env file that can be accessed by dotenv package and it also makes the project more secure.

Now on your terminal run the migrations using the below command:

sequelize db:migrate

Now, you’re all done. To run the server use the below command:

node server.js

You’ll see something like this:

GraphiQL is running on http://localhost:3000/graphiql

This means that the server is working fine.

Step -8: Testing the APIs:

Now open the Firecamp application and select the GraphQL icon from the homepage.

Let’s create a new user. To do so use the below code:

mutation{
createUser(firstName : "Vyom", lastName : "Srivastava", email : "srivastavavyom191@gmail.com", password : "12345"){
firstName,
lastName,
email
}
}

If everything is fine, you should see something like this:

gql-cms-1

Now let’s try to login using the credentials:

mutation{
login(email : "srivastavavyom191@gmail.com", password : "12345")
}

On success, you’ll get the bearer token. Copy that token, we’ll use the token to create a post.

gql-cms-2

Now to create a post using the below code:

mutation{
addPost(title : "Test Article", content : "THis is a test article", tags : []) {
id
}
}

It’s done now. We have covered a lot about our CMS, now I want you to figure out how to delete and update an existing post. It is very similar to how we created the post.

Final Words

In this article, we covered how we can create a Headless CMS using GraphQL. You can now create a front end using ReactJS or AngularJS or any tech you want to use. Being headless CMS, you now have the power to use any platform for this API.

CONTENT
PrerequisitesStep -1: Initializing the projectStep -2: Define the GraphQL SchemaStep -3: Setting up Resolvers:Step -4: Creating the Server:Step -5: Defining Models:Step -6: Create the configuration file:Step -7: Creating .env and migrations:Step -8: Testing the APIs:Final Words

Links

DownloadDocChange LogsCookiesTerms & ConditionsPrivacy PolicyContact Us

Apps & Integrations

HTTPGraphQLWebsocketSocketIO

Firecamp Newsletter