Chimezie Enyinnaya

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:

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

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

1npm 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:

1// server.js
2
3'use strict';
4const express = require('express');
5
6const bodyParser = require('body-parser');
7
8const { graphqlExpress, graphiqlExpress } = require('apollo-server-express');
9
10const schema = require('./data/schema');
11const PORT = 3000;
12// Create our express app
13
14const app = express();
15
16// Graphql endpoint
17
18app.use('/api', bodyParser.json(), graphqlExpress({ schema }));
19
20// Graphiql for testing the API out
21
22app.use('/graphiql', graphiqlExpress({ endpointURL: 'api' }));
23
24app.listen(PORT, () => {
25 console.log(`GraphiQL is running on http://localhost:${PORT}/graphiql`);
26
27});

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:

1// data/schema.js
2
3
4'use strict';
5
6const { makeExecutableSchema } = require('graphql-tools');
7
8const resolvers = require('./resolvers');
9
10// Define our schema using the GraphQL schema language
11
12const typeDefs = `
13 scalar DateTime
14
15 type User {
16
17 id: Int!
18
19 firstName: String!
20
21 lastName: String
22
23 email: String!
24
25 posts: [Post]
26
27 createdAt: DateTime! # will be generated
28
29 updatedAt: DateTime! # will be generated
30
31 }
32
33 type Post {
34
35 id: Int!
36
37 title: String!
38
39 slug: String!
40
41 content: String!
42
43 status: Boolean!
44
45 user: User!
46
47 tags: [Tag!]!
48
49 createdAt: DateTime! # will be generated
50
51 updatedAt: DateTime! # will be generated
52
53 }
54
55 type Tag {
56
57 id: Int!
58
59 name: String!
60
61 slug: String!
62
63 description: String
64
65 posts: [Post]
66
67 createdAt: DateTime! # will be generated
68
69 updatedAt: DateTime! # will be generated
70
71 }
72
73 type Query {
74
75 allUsers: [User]
76
77 fetchUser(id: Int!): User
78
79 allPosts: [Post]
80
81 fetchPost(id: Int!): Post
82
83 allTags: [Tag]
84
85 fetchTag(id: Int!): Tag
86
87 }
88
89 type Mutation {
90
91 login (
92
93 email: String!,
94
95 password: String!
96
97 ): String
98
99 createUser (
100
101 firstName: String!,
102
103 lastName: String,
104
105 email: String!,
106
107 password: String!
108
109 ): User
110
111 updateUser (
112
113 id: Int!,
114
115 firstName: String!,
116
117 lastName: String,
118
119 email: String!,
120
121 password: String!
122
123 ): User
124
125 addPost (
126
127 title: String!,
128
129 content: String!,
130
131 status: Boolean
132
133 tags: [Int!]!
134
135 ): Post
136
137 updatePost (
138
139 id: Int!,
140
141 title: String!,
142
143 content: String!,
144
145 status: Boolean,
146
147 tags: [Int!]!
148
149 ): Post
150
151 deletePost (id: Int!): Boolean
152
153 addTag (
154
155 name: String!,
156
157 description: String
158
159 ): Tag
160
161 updateTag (
162
163 id: Int!,
164
165 name: String!,
166
167 description: String
168
169 ): Tag
170
171 deleteTag (id: Int!): Boolean
172
173 }
174
175`;
176
177module.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:

1npm 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:

1sequelize 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:

1// config/config.js
2
3'use strict';
4require('dotenv').config();
5
6module.exports = {
7 "development": {
8
9 "username": process.env.DB_USERNAME,
10
11 "password": process.env.DB_PASSWORD,
12
13 "database": process.env.DB_NAME,
14
15 "host": process.env.DB_HOST,
16
17 "dialect": "mysql"
18
19 },
20
21 "production": {
22
23 "username": process.env.DB_USERNAME,
24
25 "password": process.env.DB_PASSWORD,
26
27 "database": process.env.DB_NAME,
28
29 "host": process.env.DB_HOST,
30
31 "dialect": "mysql"
32
33 }
34
35};

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:

1//.env
2
3NODE_ENV=development
4DB_HOST=localhost
5DB_USERNAME=root
6DB_PASSWORD=
7DB_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:

1// .sequelizerc
2
3const path = require('path');
4
5module.exports = {
6 'config': path.resolve('config', 'config.js')
7}

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:

1// models/index.js
2
3var 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:

1sequelize 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:

1// migrations/XXXXXXXXXXXXXX-create-user.js
2
3
4'use strict';
5
6module.exports = {
7
8 up: (queryInterface, Sequelize) => {
9
10 return queryInterface.createTable('users', {
11
12 id: {
13
14 type: Sequelize.INTEGER,
15
16 autoIncrement: true,
17
18 primaryKey: true,
19
20 allowNull: false
21
22 },
23
24 firstName: {
25
26 type: Sequelize.STRING,
27
28 allowNull: false
29
30 },
31
32 lastName: {
33
34 type: Sequelize.STRING
35
36 },
37
38 email: {
39
40 type: Sequelize.STRING,
41
42 unique: true,
43
44 allowNull: false
45
46 },
47
48 password: {
49
50 type: Sequelize.STRING,
51
52 allowNull: false
53
54 },
55
56 createdAt: {
57
58 type: Sequelize.DATE,
59
60 allowNull: false
61
62 },
63
64 updatedAt: {
65
66 type: Sequelize.DATE,
67
68 allowNull: false
69
70 }
71
72 });
73
74 },
75
76 down: (queryInterface, Sequelize) => {
77
78 return queryInterface.dropTable('Users');
79
80 }
81
82};

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

1// models/user.js
2
3
4'use strict';
5
6module.exports = (sequelize, DataTypes) => {
7
8 const User = sequelize.define('User', {
9
10 id: {
11
12 type: DataTypes.INTEGER,
13
14 primaryKey: true,
15
16 autoIncrement: true,
17
18 allowNull: false
19
20 },
21
22 firstName: {
23
24 type: DataTypes.STRING,
25
26 allowNull: false
27
28 },
29
30 lastName: DataTypes.STRING,
31
32 email: {
33
34 type: DataTypes.STRING,
35
36 unique: true,
37
38 allowNull: false
39
40 },
41
42 password: {
43
44 type: DataTypes.STRING,
45
46 allowNull: false
47
48 }
49
50 });
51
52 User.associate = function(models) {
53
54 // A user can have many post
55
56 User.hasMany(models.Post);
57
58 };
59
60 return User;
61
62};

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

We do the same for Post:

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

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

1// migrations/XXXXXXXXXXXXXX-create-post.js
2
3
4'use strict';
5
6module.exports = {
7
8 up: (queryInterface, Sequelize) => {
9
10 return queryInterface.createTable('posts', {
11
12 id: {
13
14 type: Sequelize.INTEGER,
15
16 autoIncrement: true,
17
18 primaryKey: true,
19
20 allowNull: false
21
22 },
23
24 userId: {
25
26 type: Sequelize.INTEGER.UNSIGNED,
27
28 allowNull: false
29
30 },
31
32 title: {
33
34 type: Sequelize.STRING,
35
36 allowNull: false
37
38 },
39
40 slug: {
41
42 type: Sequelize.STRING,
43
44 unique: true,
45
46 allowNull: false
47
48 },
49
50 content: {
51
52 type: Sequelize.STRING,
53
54 allowNull: false
55
56 },
57
58 status: {
59
60 type: Sequelize.BOOLEAN,
61
62 allowNull: false,
63
64 defaultValue: false
65
66 },
67
68 createdAt: {
69
70 type: Sequelize.DATE,
71
72 allowNull: false
73
74 },
75
76 updatedAt: {
77
78 type: Sequelize.DATE,
79
80 allowNull: false
81
82 }
83
84 });
85
86 },
87
88 down: (queryInterface, Sequelize) => {
89
90 return queryInterface.dropTable('posts');
91
92 }
93
94};

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

1// models/post.js
2
3
4'use strict';
5
6module.exports = (sequelize, DataTypes) => {
7
8 const Post = sequelize.define('Post', {
9
10 id: {
11
12 type: DataTypes.INTEGER,
13
14 primaryKey: true,
15
16 autoIncrement: true,
17
18 allowNull: false
19
20 },
21
22 userId: {
23
24 type: DataTypes.INTEGER.UNSIGNED,
25
26 allowNull: false
27
28 },
29
30 title: {
31
32 type: DataTypes.STRING,
33
34 allowNull: false
35
36 },
37
38 slug: {
39
40 type: DataTypes.STRING,
41
42 allowNull: false,
43
44 unique: true
45
46 },
47
48 content: {
49
50 type: DataTypes.STRING,
51
52 allowNull: false
53
54 },
55
56 status: {
57
58 type: DataTypes.BOOLEAN,
59
60 allowNull: false,
61
62 defaultValue: false
63
64 }
65
66 });
67
68 Post.associate = function(models) {
69
70 // A post belongs to a user
71
72 Post.belongsTo(models.User);
73
74 // A post can belong to many tags
75
76 Post.belongsToMany(models.Tag, { through: 'post_tag' });
77
78 };
79
80 return Post;
81
82};

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:

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

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

1// migrations/XXXXXXXXXXXXXX-create-tag.js
2
3
4'use strict';
5
6module.exports = {
7
8 up: (queryInterface, Sequelize) => {
9
10 return queryInterface.createTable('tags', {
11
12 id: {
13
14 type: Sequelize.INTEGER,
15
16 autoIncrement: true,
17
18 primaryKey: true,
19
20 allowNull: false
21
22 },
23
24 name: {
25
26 type: Sequelize.STRING,
27
28 unique: true,
29
30 allowNull: false
31
32 },
33
34 slug: {
35
36 type: Sequelize.STRING,
37
38 unique: true,
39
40 allowNull: false
41
42 },
43
44 description: {
45
46 type: Sequelize.STRING,
47
48 },
49
50 createdAt: {
51
52 type: Sequelize.DATE,
53
54 allowNull: false
55
56 },
57
58 updatedAt: {
59
60 type: Sequelize.DATE,
61
62 allowNull: false
63
64 }
65
66 });
67
68 },
69
70 down: (queryInterface, Sequelize) => {
71
72 return queryInterface.dropTable('tags');
73
74 }
75
76};

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

1// models/tag.js
2
3
4'use strict';
5
6module.exports = (sequelize, DataTypes) => {
7
8 const Tag = sequelize.define('Tag', {
9
10 id: {
11
12 type: DataTypes.INTEGER,
13
14 primaryKey: true,
15
16 autoIncrement: true,
17
18 allowNull: false
19
20 },
21
22 name: {
23
24 type: DataTypes.STRING,
25
26 unique: true,
27
28 allowNull: false
29
30 },
31
32 slug: {
33
34 type: DataTypes.STRING,
35
36 allowNull: false,
37
38 unique: true
39
40 },
41
42 description: DataTypes.STRING
43
44 });
45
46 Tag.associate = function(models) {
47
48 // A tag can have to many posts
49
50 Tag.belongsToMany(models.Post, { through: 'post_tag' });
51
52 };
53
54 return Tag;
55
56};

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.

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

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

1// migrations/XXXXXXXXXXXXXX-create-post-tag.js
2
3
4'use strict';
5
6module.exports = {
7
8 up: (queryInterface, Sequelize) => {
9
10 return queryInterface.createTable('post_tag', {
11
12 id: {
13
14 type: Sequelize.INTEGER,
15
16 autoIncrement: true,
17
18 primaryKey: true,
19
20 allowNull: false
21
22 },
23
24 postId: {
25
26 type: Sequelize.INTEGER,
27
28 allowNull: false
29
30 },
31
32 tagId: {
33
34 type: Sequelize.INTEGER,
35
36 allowNull: false
37
38 },
39
40 createdAt: {
41
42 allowNull: false,
43
44 type: Sequelize.DATE
45
46 },
47
48 updatedAt: {
49
50 allowNull: false,
51
52 type: Sequelize.DATE
53
54 }
55
56 });
57
58 },
59
60 down: (queryInterface, Sequelize) => {
61
62 return queryInterface.dropTable('post_tag');
63
64 }
65
66};

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

1// models/posttag.js
2
3
4'use strict';
5module.exports = (sequelize, DataTypes) => {
6
7 const PostTag = sequelize.define('PostTag', {
8
9 id: {
10
11 type: DataTypes.INTEGER,
12
13 primaryKey: true,
14
15 autoIncrement: true,
16
17 allowNull: false
18
19 },
20
21 postId:{
22
23 type: DataTypes.INTEGER.UNSIGNED,
24
25 allowNull: false
26
27 },
28
29 tagId:{
30
31 type: DataTypes.INTEGER.UNSIGNED,
32
33 allowNull: false
34
35 }
36
37 });
38 return PostTag;
39
40};

Now, let’s run our migrations:

1sequelize 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:

1// data/resolvers.js
2
3'use strict';
4
5
6const { GraphQLScalarType } = require('graphql');
7
8const { Kind } = require('graphql/language');
9
10const { User, Post, Tag } = require('../models');
11
12const bcrypt = require('bcrypt');
13
14const jwt = require('jsonwebtoken');
15
16const slugify = require('slugify');
17
18require('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:

1// data/resolvers.js
2
3
4// Define resolvers
5
6const resolvers = {
7
8 Query: {
9
10 // Fetch all users
11
12 async allUsers() {
13
14 return await User.all();
15
16 },
17
18
19 // Get a user by it ID
20
21 async fetchUser(_, { id }) {
22
23 return await User.findById(id);
24
25 },
26
27 // Fetch all posts
28
29 async allPosts() {
30
31 return await Post.all();
32
33 },
34
35 // Get a post by it ID
36
37 async fetchPost(_, { id }) {
38
39 return await Post.findById(id);
40
41 },
42
43 // Fetch all tags
44
45 async allTags(_, args, { user }) {
46 return await Tag.all();
47
48 },
49
50 // Get a tag by it ID
51
52 async fetchTag(_, { id }) {
53
54 return await Tag.findById(id);
55
56 },
57
58 },
59}
60
61module.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:

1// data/resolvers.js
2
3
4Mutation: {
5
6 // Handles user login
7
8 async login(_, { email, password }) {
9
10 const user = await User.findOne({ where: { email } });
11
12 if (!user) {
13
14 throw new Error('No user with that email');
15
16 }
17
18 const valid = await bcrypt.compare(password, user.password);
19
20 if (!valid) {
21
22 throw new Error('Incorrect password');
23
24 }
25
26 // Return json web token
27
28 return jwt.sign({
29
30 id: user.id,
31
32 email: user.email
33
34 }, process.env.JWT_SECRET, { expiresIn: '1y' });
35
36 },
37
38 // Create new user
39
40 async createUser(_, { firstName, lastName, email, password }) {
41
42 return await User.create({
43
44 firstName,
45
46 lastName,
47
48 email,
49
50 password: await bcrypt.hash(password, 10)
51
52 });
53
54 },
55
56 // Update a particular user
57
58 async updateUser(_, { id, firstName, lastName, email, password }, { authUser }) {
59
60 // Make sure user is logged in
61
62 if (!authUser) {
63
64 throw new Error('You must log in to continue!')
65
66 }
67
68 // fetch the user by it ID
69
70 const user = await User.findById(id);
71
72 // Update the user
73
74 await user.update({
75
76 firstName,
77
78 lastName,
79
80 email,
81
82 password: await bcrypt.hash(password, 10)
83
84 });
85
86 return user;
87
88 },
89
90 // Add a new post
91
92 async addPost(_, { title, content, status, tags }, { authUser }) {
93
94 // Make sure user is logged in
95
96 if (!authUser) {
97
98 throw new Error('You must log in to continue!')
99
100 }
101
102 const user = await User.findOne({ where: { id: authUser.id } });
103 const post = await Post.create({
104
105 userId: user.id,
106
107 title,
108
109 slug: slugify(title, { lower: true }),
110
111 content,
112
113 status
114
115 });
116
117 // Assign tags to post
118
119 await post.setTags(tags);
120
121 return post;
122
123 },
124
125 // Update a particular post
126
127 async updatePost(_, { id, title, content, status, tags }, { authUser }) {
128
129 // Make sure user is logged in
130
131 if (!authUser) {
132
133 throw new Error('You must log in to continue!')
134
135 }
136
137 // fetch the post by it ID
138
139 const post = await Post.findById(id);
140
141 // Update the post
142
143 await post.update({
144
145 title,
146
147 slug: slugify(title, { lower: true }),
148
149 content,
150
151 status
152
153 });
154
155 // Assign tags to post
156
157 await post.setTags(tags);
158
159 return post;
160
161 },
162
163 // Delete a specified post
164
165 async deletePost(_, { id }, { authUser }) {
166
167 // Make sure user is logged in
168
169 if (!authUser) {
170
171 throw new Error('You must log in to continue!')
172
173 }
174
175 // fetch the post by it ID
176
177 const post = await Post.findById(id);
178
179 return await post.destroy();
180
181 },
182
183 // Add a new tag
184
185 async addTag(_, { name, description }, { authUser }) {
186
187 // Make sure user is logged in
188
189 if (!authUser) {
190
191 throw new Error('You must log in to continue!')
192
193 }
194
195 return await Tag.create({
196
197 name,
198
199 slug: slugify(name, { lower: true }),
200
201 description
202
203 });
204
205 },
206
207 // Update a particular tag
208
209 async updateTag(_, { id, name, description }, { authUser }) {
210
211 // Make sure user is logged in
212
213 if (!authUser) {
214
215 throw new Error('You must log in to continue!')
216
217 }
218
219 // fetch the tag by it ID
220
221 const tag = await Tag.findById(id);
222
223 // Update the tag
224
225 await tag.update({
226
227 name,
228
229 slug: slugify(name, { lower: true }),
230
231 description
232
233 });
234
235 return tag;
236
237 },
238
239 // Delete a specified tag
240
241 async deleteTag(_, { id }, { authUser }) {
242
243 // Make sure user is logged in
244
245 if (!authUser) {
246
247 throw new Error('You must log in to continue!')
248
249 }
250
251 // fetch the tag by it ID
252
253 const tag = await Tag.findById(id);
254
255 return await tag.destroy();
256
257 }
258
259},

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:

1// data/resolvers.js
2
3
4User: {
5
6 // Fetch all posts created by a user
7
8 async posts(user) {
9
10 return await user.getPosts();
11
12 }
13
14},
15
16Post: {
17
18 // Fetch the author of a particular post
19
20 async user(post) {
21
22 return await post.getUser();
23
24 },
25
26 // Fetch alls tags that a post belongs to
27
28 async tags(post) {
29
30 return await post.getTags();
31
32 }
33
34},
35
36Tag: {
37
38 // Fetch all posts belonging to a tag
39
40 async posts(tag) {
41
42 return await tag.getPosts();
43
44 }
45
46},

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:

1// data/resolvers.js
2
3
4DateTime: new GraphQLScalarType({
5
6 name: 'DateTime',
7
8 description: 'DateTime type',
9
10 parseValue(value) {
11
12 // value from the client
13
14 return new Date(value);
15
16 },
17
18 serialize(value) {
19
20 const date = new Date(value);
21
22 // value sent to the client
23
24 return date.toISOString();
25
26 },
27
28 parseLiteral(ast) {
29
30 if (ast.kind === Kind.INT) {
31
32 // ast value is always in string format
33
34 return parseInt(ast.value, 10);
35
36 }
37
38 return null;
39
40 }
41
42})

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:

1// .env
2
3JWT_SECRET=somereallylongsecretkey

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

1// server.js
2
3
4'use strict';
5
6const express = require('express');
7
8const bodyParser = require('body-parser');
9
10const { graphqlExpress, graphiqlExpress } = require('apollo-server-express');
11
12const schema = require('./data/schema');
13
14**const jwt = require('express-jwt');**
15
16**require('dotenv').config();**
17
18const PORT = 3000;
19
20// Create our express app
21
22const app = express();
23
24// Graphql endpoint
25
26app.use('/api', bodyParser.json(), **jwt({**
27
28** secret: process.env.JWT_SECRET,**
29
30** credentialsRequired: false,**
31
32** })**, graphqlExpress( **req => ({**
33
34** schema,**
35
36** context: {**
37
38** authUser: req.user**
39
40** }**
41
42**})**));
43
44// Graphiql for testing the API out
45
46app.use('/graphiql', graphiqlExpress({ endpointURL: 'api' }));
47
48app.listen(PORT, () => {
49
50 console.log(`GraphiQL is running on http://localhost:${PORT}/graphiql`);
51
52});

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:

1node 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 userCreate new user

We can now login:

Login a userLogin 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 headerAdd JWT to header

Now, try adding a new post:

Add a new postAdd a new post

Fetch a particular postFetch 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.

Stratus Background
StratusUpdate

Sign up for the Stratus Update newsletter

With our monthly newsletter, we’ll keep you up to date with a curated selection of the latest cloud services, projects and best practices.
Click here to read the latest issue.