Build a CMS API With GraphQL and Apollo Server

Chimezie Enyinnaya
manifoldco
Published in
17 min readJan 18, 2018

We will be building an API for a Blog CMS. The Blog will comprise of three concepts: Users, Posts and Tags. The CMS will handle creating and authenticating users (using JWT). Tags will be used as a taxonomy to group posts, think of it like categories in WordPress. A post can belong to many tags and a tag can have many posts. Authenticated users will be able to perform CRUD tasks like creating posts and tags.

This tutorial assumes you already have some basic understanding of GraphQL. You might want to go over GraphQL docs as a refresher.

With that said, let’s get started!

Create a New Project

We’ll start by creating a new Node.js project, we’ll call it graphql-blog-cms-api:

mkdir graphql-blog-cms-api && cd graphql-blog-cms-api
npm init -y

Once the app is created, we need to install project’s dependencies:

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

We’ll go over each of package as we get to them. With the dependencies installed, let’s start fleshing out the app by creating a GraphQL server.

Create GraphQL Server

Create a new server.js file and paste the code below into it:

// server.js'use strict';
const express = require('express');
const bodyParser = require('body-parser');const { graphqlExpress, graphiqlExpress } = require('apollo-server-express');const schema = require('./data/schema');
const PORT = 3000;
// Create our express app
const app = express();// Graphql endpointapp.use('/api', bodyParser.json(), graphqlExpress({ schema }));// Graphiql for testing the API outapp.use('/graphiql', graphiqlExpress({ endpointURL: 'api' }));app.listen(PORT, () => {
console.log(`GraphiQL is running on http://localhost:${PORT}/graphiql`);
});

We import our dependencies, express is the Node.js framework of choice for this tutorial. body-parser is used to parse incoming request body. graphqlExpress is the express implementation of Apollo server which will be used to power our GraphQL server. With graphiqlExpress, we will be able to use GraphiQL which is an in-browser IDE for exploring GraphQL (we’ll use this to test out the GraphQL API). Lastly, we import our GraphQL schema which we’ll created shortly.

We define a port that the server will listen on. We then create an express app.

We define the route for our GraphQL API. We add body-parser middleware to the route. We also addgraphqlExpress passing along the GraphQL schema.

Then we define the route for GraphiQL passing to it the GraphQL endpoint we created above.

Finally, we start the server and listen on the port defined above.

Define GraphQL Schema

Let’s move on to defining our GraphQL schema. Schemas describe how data are shaped and what data on the server can be queried. GraphQL schemas are strongly typed, hence all the object defined in a schema must have types. Schemas can be of two types: Query and Mutation.

Create a folder name data and within this folder, create a new schema.js file, then paste the code below into it:

// data/schema.js
'use strict';
const { makeExecutableSchema } = require('graphql-tools');const resolvers = require('./resolvers');// Define our schema using the GraphQL schema languageconst 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 });

We start off by pulling in graphql-tools, a package by the Apollo team. This package allows us to define our schema using the GraphQL schema language. We also import our resolvers which we’ll create shortly. We then begin to define the schema. We start by defining a custom scalar type called DateTime because Date is part of the types GraphQL support out of the box. So we need to define it ourselves. The DateTime will be used for the createdAt and updatedAt fields respectively. The createdAt and updatedAt fields will be auto generated at the point of creating our defined types.

We define the User type. Its fields are pretty straightforward. Notice the posts field as it will be an array of all the posts a user has created. User and Post have a one-to-many relationship, that is, a user can have many posts and on the other hand, a post can only belong to one user.

We then define the Post type. Its fields are pretty straightforward The user is a required field and represent the user that created a post. The tags field is an array of tags a post belongs to. [Tag!]! signifies that the array can not be empty. This means a post must belong to at least one tag. Post and Tag have a belongs-to-many relationship, that is, a post can belong to many tags and on the other hand, a tag can have many posts.

Then we define the Tag type. Again, its fields are pretty straightforward. The posts field is an array of posts a tag has.

Having defined our types, we move on to define the queries that can be performed on these types. allUsers will fetch all the users created and return them in an array. fetchUser(id: Int!) will fetch a user with a specified ID. We do the same for Post and Tag respectively.

Next, we define some mutations. While queries are used for fetching data from the server, mutations are used to add/modify data on the server. We define a login mutation which takes email address and password as inputs. It is use to authenticate users. We also define mutations to create and update User, Post and Tag respectively. The update mutations in addition to the data, also accept the ID of the type (User, Post, Tag) we want to update. Lastly, we define mutations for deleting a Post and a Tag respectively.

Finally, we use makeExecutableSchema to build the schema, passing to it our schema and the resolvers.

Setting Up Database

As earlier mentioned, we’ll be using MySQL for the purpose of this tutorial. Also, we’ll be using Sequelize as our ORM. We have installed the necessary dependencies for both of these. Now, we need to install Sequelize CLI on our computer. We’ll install it globally:

npm install –g sequelize-cli

Once it’s installed, we can then initialize Sequelize in our project. With the project’s root directory, run the command below:

sequelize init

This will create following folders:

  • config: contains config file, which tells CLI how to connect with database
  • models: contains all models for your project, also contains an index.js file which integrates all the models together.
  • migrations: contains all migration files
  • seeders: contains all seed files

Ignore the seeders folder as we won’t be creating any seeders in this tutorial. The config folder contain a JSON file config.json. We’ll rename this file to config.js. Now, open config/config.js and paste the snippet below into it:

// config/config.js'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" }, "production": { "username": process.env.DB_USERNAME, "password": process.env.DB_PASSWORD, "database": process.env.DB_NAME, "host": process.env.DB_HOST, "dialect": "mysql" }};

Notice we are using the dotenv package to read our database details from an .env file. Let’s create a .env file and paste the snippet below into it:

//.envNODE_ENV=development
DB_HOST=localhost
DB_USERNAME=root
DB_PASSWORD=
DB_NAME=graphql_blog_cms

Update accordingly with your own database details.

Because we have changed the config file from JSON to JavaScript file, we need to make the Sequelize CLI aware of this. We can do that by creating a .sequelizerc file and paste the snippet below in it:

// .sequelizercconst path = require('path');module.exports = {
'config': path.resolve('config', 'config.js')
}

Now the CLI will be aware of our changes.

One last thing we need to do is update models/index.js to also reference config/config.js. Replace the line where the config file is imported with the line below:

// models/index.jsvar config    = require(__dirname + '/../config/config.js')[env];

Creating Models and Migrations

With our database setup, now create our models and their corresponding migrations. For consistency, we want our models to mirror our GraphQL schema. So we are going to create 3 models (User, Post and Tag) with the corresponding fields we defined on our schema. We’ll be using the Sequelize CLI for this.

We’ll start with User, run the command below:

sequelize model:generate --name User --attributes \ firstName:string,lastName:string,email:string

This will do following:

  • Create a model file user.js in models folder
  • Create a migration file with name like XXXXXXXXXXXXXX-create-user.js in migrations folder

Open migrations/XXXXXXXXXXXXXX-create-user.js and replace it content with:

// migrations/XXXXXXXXXXXXXX-create-user.js
'use strict';module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.createTable('users', { id: { type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true, allowNull: false }, firstName: { type: Sequelize.STRING, allowNull: false }, lastName: { type: Sequelize.STRING }, email: { type: Sequelize.STRING, unique: true, allowNull: false }, password: { type: Sequelize.STRING, allowNull: false }, createdAt: { type: Sequelize.DATE, allowNull: false }, updatedAt: { type: Sequelize.DATE, allowNull: false } }); }, down: (queryInterface, Sequelize) => { return queryInterface.dropTable('Users'); }};

Also, replace the content of models/user.js with:

// models/user.js
'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;};

Notice we define the relationship (one-to-many) between User and Post.

We do the same for Post:

sequelize model:generate --name Post --attributes title:string,content:string

Open migrations/XXXXXXXXXXXXXX-create-post.js and replace it content with:

// migrations/XXXXXXXXXXXXXX-create-post.js
'use strict';module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.createTable('posts', { id: { type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true, allowNull: false }, userId: { type: Sequelize.INTEGER.UNSIGNED, allowNull: false }, title: { type: Sequelize.STRING, allowNull: false }, slug: { type: Sequelize.STRING, unique: true, allowNull: false }, content: { type: Sequelize.STRING, allowNull: false }, status: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false }, createdAt: { type: Sequelize.DATE, allowNull: false }, updatedAt: { type: Sequelize.DATE, allowNull: false } }); }, down: (queryInterface, Sequelize) => { return queryInterface.dropTable('posts'); }};

Also, replace the content of models/post.js with:

// 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;};

We define the inverse relationship between Post and User. Also, we define the relationship (belongs-to-many) between Post and Tag.

We do the same for Tag:

sequelize model:generate --name Tag --attributes \ name:string,description:string

Open migrations/XXXXXXXXXXXXXX-create-tag.js and replace it content with:

// migrations/XXXXXXXXXXXXXX-create-tag.js
'use strict';module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.createTable('tags', { id: { type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true, allowNull: false }, name: { type: Sequelize.STRING, unique: true, allowNull: false }, slug: { type: Sequelize.STRING, unique: true, allowNull: false }, description: { type: Sequelize.STRING, }, createdAt: { type: Sequelize.DATE, allowNull: false }, updatedAt: { type: Sequelize.DATE, allowNull: false } }); }, down: (queryInterface, Sequelize) => { return queryInterface.dropTable('tags'); }};

Also, replace the content of models/tag.js with:

// 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 to many posts Tag.belongsToMany(models.Post, { through: 'post_tag' }); }; return Tag;};

Also, we define the relationship (belongs-to-many)between Tag and Post.

We need to define one more model/migration for the pivot table for the belongs-to-many relationship between Tag and Post.

sequelize model:generate --name PostTag --attributes postId:integer

Open migrations/XXXXXXXXXXXXXX-create-post-tag.js and replace it content with:

// migrations/XXXXXXXXXXXXXX-create-post-tag.js
'use strict';module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.createTable('post_tag', { id: { type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true, allowNull: false }, postId: { type: Sequelize.INTEGER, allowNull: false }, tagId: { type: Sequelize.INTEGER, allowNull: false }, createdAt: { allowNull: false, type: Sequelize.DATE }, updatedAt: { allowNull: false, type: Sequelize.DATE } }); }, down: (queryInterface, Sequelize) => { return queryInterface.dropTable('post_tag'); }};

Also, replace the content of models/posttag.js with:

// 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 } });
return PostTag;
};

Now, let’s run our migrations:

sequelize db:migrate

Writing Resolvers

Our schema is nothing without resolvers. A resolver is a function that defines how a field in a schema is executed. Now, let’s we define our resolvers. Within the data folder, create a new resolvers.js file and paste following code into it:

// 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();

We start off by importing the necessary packages as well as our models. Because we’ll be defining a custom scalar DateTime type, we import GraphQLScalarType and kind. bcrypt will be used for hashing users password, jsonwebtoken will be used to generate a JSON Web Token (JWT) which will be used to authenticate users. slugify will be used to create slugs. We also import our models. Finally, import dotenv so we can read from our .env file.

Now let’s start defining our resolver functions. We’ll start by defining resolver functions for our queries. Add the code below inside resolvers.js:

// data/resolvers.js
// Define resolversconst 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); }, },
}
module.exports = resolvers;

Our resolver functions makes use of JavaScript new features like object destructuring and async/await. The resolvers for queries are pretty straightforward as they simply retrieve data from the database.

Now, let’s define resolver functions for our mutations. Add the code below inside resolvers.js just after the Query object:

// data/resolvers.js
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(); }},

Let’s go over the mutations. login checks if a user with the email and password supplied exists in the database. We use bcrypt to compare the password supplied with the password hash generated while creating the user. If the user exist, we generate a JWT. createUser simply adds a new user to the database with the data passed to it. As you can see we hash the user password with bcrypt. For the other mutations, we first check to make sure the user is actually logged in before allowing to go on and carry out the intended tasks. addPost and updatePost after adding/updating a post to the database uses setTags() to assign tags to the post. setTags() is available on the model due to the belongs-to-many relationship between Post and Tag. We also define resolvers to add, update and delete a tag respectively.

Next, we define resolvers to retrieve the fields on our User, Post and Tag type respectively. Add the code below inside resolvers.js just after the Mutation object:

// data/resolvers.js
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(); }},

These uses the methods (getPosts(), getUser(), getTags(), getPosts()) made available on the models due to the relationships we defined.

Let’s define our custom scalar type. Add the code below inside resolvers.js just after the Tag object:

// data/resolvers.js
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; }})

We define our custom scalar DateTime type. parseValue() accepts a value from the client and convert it to a Date object which will be inserted into the database. serialize() also accepts a value, but this time value is coming from the database. The value converted to a Date object and a date in ISO format is returned to the client.

That’s all for our resolvers. Noticed we use JWT_SECRET from the environment variable which we are yet to define. Add the line below to .env:

// .envJWT_SECRET=somereallylongsecretkey

One last thing to do before we test out the API is to update server.js as below:

// server.js
'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 appconst app = express();// Graphql endpointapp.use('/api', bodyParser.json(), jwt({ secret: process.env.JWT_SECRET, credentialsRequired: false, }), graphqlExpress( req => ({ schema, context: { authUser: req.user }})));// Graphiql for testing the API outapp.use('/graphiql', graphiqlExpress({ endpointURL: 'api' }));app.listen(PORT, () => { console.log(`GraphiQL is running on http://localhost:${PORT}/graphiql`);});

We simply add the express-jwt middleware to the API route. This makes the route secured as it will check to see if there is an Authorization header with a JWT on the request before granting access to the route. We set credentialsRequired to false because we users to be able to at least login and register first. express-jwt adds the details of the authenticated user to the request body which we turn pass as context to graphqlExpress.

Testing It Out

Now, we can test out the API. We’ll use GraphiQL to testing out the API. First, we need to start the server with:

node server.js

and we can access it on http://localhost:3000/graphiql. Try creating a new user with createUser mutation. You should get a response a s in the image below:

Create new user

We can now login:

Login a user

You can see the JWT returned on successful login.

For the purpose of testing out the other secured aspects of the API, we need to find a way to add the JWT generated above to the request headers. To do that, we’ll use a Chrome extension called ModHeader to modify the request headers and define the Authorization header. Once the Authorization header contains a JWT, this signifies that the user making the request is authenticated, hence will be able to carry out authenticated users only activities.

Enter the Authorization as the name of the header and Bearer YOUR_JSON_WEB_TOKEN as its value:

Add JWT to header

Now, try adding a new post:

Add a new post
Fetch a particular post

Conclusion

We’ve established an API to power our own blog with GraphQL interface and can make authenticated calls to create and retrieve data, now what can you do?

The complete code for our API is available on GitHub and can be used as a starting point.

Check out Deploying Apollo GQL API to Zeit which shows how to take your local implementation of an API and making it accessible on the web using Zeit Now.

Then read about pairing graphql fragments with UI components in GraphQL Fragments are the Best Match for UI Components and start building the interface of your blog.

--

--