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:
```bash
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:
```bash
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:
```js
// 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 endpoint
app.use('/api', bodyParser.json(), graphqlExpress({ schema }));
// 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`);
});
```
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:
```js
// data/schema.js
'use strict';
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 });
```
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:
```bash
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:
```js
// 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:
```env
//.env
NODE_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
```js
// .sequelizerc
const 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:
```js
// models/index.js
var 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:
```bash
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:
```js
// 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:
```js
// 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:
```bash
sequelize model:generate --name Post --attributes title:string,content:string
```
Open `migrations/XXXXXXXXXXXXXX-create-post.js` and replace it content with:
```js
// 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:
```js
// 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`:
```bash
sequelize model:generate --name Tag --attributes \ name:string,description:string
```
Open `migrations/XXXXXXXXXXXXXX-create-tag.js` and replace it content with:
```js
// 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:
```js
// 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`.
```bash
sequelize model:generate --name PostTag --attributes postId:integer
```
Open `migrations/XXXXXXXXXXXXXX-create-post-tag.js` and replace it content with:
```js
// 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:
```js
// 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:
```bash
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:
```js
// 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`:
```js
// data/resolvers.js
// 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);
},
},
}
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:
```js
// 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:
```js
// 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:
```js
// 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`:
```env
// .env
JWT_SECRET=somereallylongsecretkey
```
One last thing to do before we test out the API is to update `server.js` as below:
```js
// 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 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`);
});
```
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:
```bash
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:

We can now login:

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:

Now, try adding a new 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.