In this episode, we are going to build a GraphQL backend using apollo-server that interacts with our business logic layer.

Adding GraphQL

We’ll start the server by using apollo-server. Apollo server implements a spec-compliant GraphQL server, which of course means that it can be queried by any GraphQL client, not just Apollo client.

We will add the necessary libraries. As dependencies for our back-end, we will need graphql and apollo-server.

yarn add graphql
yarn add @types/graphql
yarn add apollo-server 
yarn add graphql-type-datetime

This will give us the starting point for our back-end using TypeScript. We are also using a library for the graphql datetime type that we are going to use. Let's start by getting the simplest possible GraphQL server working.

If we use the code from the Getting Started section in the Apollo Server GraphQL docs, we have list of books and the Book type. Let's run the server, and then we can see that we can already fetch some data from our back-end server. We will go over the details in a moment.

It launches with the GraphQL Playground, so we can use it and immediately make queries against our server. That's awesome!

In this case we have the type Book with fields title and author. There’s a single query, books, that returns a list of Books. Rather than getting the data from the back-end, we are returning some hard-coded books in our resolver. We will ultimately use TypeORM models here, to talk with the database, get the objects and return them. Now that we’ve got a working starting point, let’s start creating our GraphQL types.

Creating our Queries and Types

We will add our types on the typeDefs variable. Category, Thread, Post and User are our types.

const typeDefs = gql`

type Category {
  id: ID!
  title: String!
  slug: String!
  insertedAt: String!
  updatedAt: String!
}
 
type Thread {
  id: ID!
  title: String!
  slug: String!
  category: Category!
  posts: [Post!]!
  insertedAt: String!
  updatedAt: String!
}

type Post {
  id: ID!
  body: String!
  thread: Thread!
  user: User!
  insertedAt: String!
  updatedAt: String!
}
 
type User {
  id: ID!
  email: String!
  name: String!
  username: String!
  insertedAt: String!
  updatedAt: String!
}

 type Query {
   threads: [Thread!]!
   categories: [Category!]!
   posts: [Post!]!
   users: [User!]!
 }
`

For now, our date times fields are strings, in the next step we are going to use the DateTime type from the package we added above. Let's start the server again and see our new types in the GraphQL Playground documentation.

$ yarn dev
🚀  Server ready at http://localhost:4000/

We can see the types in our documentation. This is really interesting, and this is one of the good things GraphQL provide to us: a good documentation.

Modularizing the Types and Resolvers

All our types and resolvers are tied up together in the variable typeDefs. We can create different files for each of them.

We're going to use merge-graphql-schemas to split our schema into different files. This feels a lot better than using a single variable to hold the entire schema.

import path from 'path'
import { fileLoader, mergeTypes, mergeResolvers } from 'merge-graphql-schemas'
import { makeExecutableSchema } from 'graphql-tools'

const typesArray = fileLoader(path.join(__dirname, './Types'))
const typeDefs = mergeTypes(typesArray, { all: true })
const resolvers = mergeResolvers(fileLoader(path.join(__dirname, './Resolvers')))
const schema = makeExecutableSchema({ typeDefs, resolvers })

...
const server = new ApolloServer({
  schema
})

The type definitions and resolvers are joined by reading all the files in the folder types and resolvers. For the resolvers, we are using the method mergeResolvers. After having type definitions and resolvers, we use the method makeExecutableSchema, it receives the type definitions and the resolvers we have created, returning the schema. We can pass this schema to the ApolloServer.

Let's split them into files.

Types/Category.ts

Starting by creating the category type, and the query we have to find a category by id and the mutation to create a category by passing a title as string.

export default `
scalar DateTime

type Category {
  id: ID!
  title: String!
  slug: String!
  threads: [Thread]
  insertedAt: DateTime!
  updatedAt: DateTime!
}

type Query {
  category(id: ID!): Category
}

type Mutation {
  createCategory(title: String!): Category
}
`

`

And we can create our resolver that right now is just returning the categories we have created.

Resolvers/CategoryResolver.ts

We’ll hardcode a mock category. For now, we are not hitting the database. We are just returning the category in the query and the mutation we have.

import { Category } from '../entity/Category'

const category = { id: 'uuid', title: 'My Category', slug: 'my-category' }

export default {
  Query: {
    category: (parent, args, {}) => {
      return category
    }
  },
  Mutation: {
    createCategory: (parent, args, {}) => {
      return category
    }
  }
}
Types/Thread.ts

We will create our Thread type. It has title, slug, category, posts, createdAt and insertedAt fields. The thread query looks up a thread by id, and createThread creates a thread. To create a thread, we are going to pass the body of the first post, because we are going to create the first post once we create the thread.

export default `
scalar DateTime

type Thread {
  id: ID!
  title: String!
  slug: String!
  category: Category
  posts: [Post]
  insertedAt: DateTime!
  updatedAt: DateTime! 
}

type Query {
  thread(id: ID!): Thread
}

type Mutation {
  createThread(title: String!, categoryId: ID!, body: String!): Thread
}
`
Resolvers/ThreadResolver.ts

The ThreadResolver is similar to the CategoryResolver. We are going to return a hardcoded thread for the query and on the mutation.

import { Thread } from '../entity/Thread'

const thread = { id: 'uuid', title: 'My Thread', slug: 'my-thread' }

export default {
  Query: {
    thread: (parent, args, {}) => {
      return thread
    }
  },
  Mutation: {
    createThread: (parent, args, {}) => {
      return thread
    }
  }
}
Types/Post.ts

In our Post, we will have body, thread, user, insertedAt and updatedAt fields. We will not have queries and we will just have one mutation to create a post by passing the body and the thread id.

export default `
scalar  DateTime

type Post {
  id: ID!
  body: String!
  thread: Thread
  user: User
  insertedAt: DateTime!
  updatedAt: DateTime!
}

type Mutation {
  createPost(body: String!, threadId: ID!): Post
}
`
Resolvers/PostResolver.ts

And we have its corresponding resolver.

import { Post } from '../entity/Post'

const post = { id: 'uuid', body: 'Body of my Text' }

export default {
  Mutation: {
    createPost: (parent, args, {}) => {
      return post
    }
  }
}
Types/User.ts

Extract the user type.

export default `
scalar DateTime

type User {
  id: ID!
  email: String!
  name: String!
  username: String!
  avatarUrl: String!
  posts: [Post]
  insertedAt: DateTime!
  updatedAt: DateTime!
}

type Query {
  users: [Users]
}

type Mutation {
  createUser(name: String!, username: String!, email: String!, password: String!): User
  authenticate(email: String!, password: String!): String
}
`
Resolvers/UserResolver.ts

The User resolver will also be in place.

import { User } from '../entity/User'

const user = { id: 'uuid', email: 'email@example.com', name: 'Name', username: 'Username' }

export default {
  Query: {
    users: (parent, args, {}) => {
      return [user, user]
    }
  },
  Mutation: {
    createUser: (parent, args, {}) => {
      return user
    },
    authenticate: (parent, args, {}) => {
      return 'TOKEN'
    }
  }
}

Now, if we restart our server, we can see the queries and types on the documentation. This is really cool. We can play with the queries we have, we can get categories, threads, and users. Remember, this isn’t hitting the database yet - this is just returning hardcoded data.

Returning data from the database

We have our types working and we have a simple query that returns an array of objects. We need to make it retrieve data from our database. Where should these calls to the database be located? The answer is: on each resolver we have created, we need to implement calls to the TypeORM methods, and then it will return the data from the database. So, we will need to add these changes. While we are going to make these changes in our resolvers, we will also implement some necessary methods to also find objects and create posts and threads.

Remember that when we add new methods, we also need to add these methods to the Types and Schema.

Resolvers/CategoryResolver.ts

In the category, we are going to implement the method to get a specific category by id. We are also joining it with threads and posts to have those in the result. We are ordering the categories by insertedAt.

We have relationships between our types, for example a Category has many threads. This means that when we return a category, the user should be able to query the threads inside of a category.

To create a category, we are getting the current connection and passing the category and its args. In the category arguments, we are changing the slug to a slug that we created by a Helper.

import { Category } from '../entity/Category'
import { getConnection } from 'typeorm'
import * as SlugHelper from '../Helpers/SlugHelper'

export default {
  Query: {
    category: (parent, { id } , {}) => {
      return Category.createQueryBuilder('category')
      .where(`category.id = '${id}'`)
      .leftJoinAndSelect('category.threads', 'threads')
      .leftJoinAndSelect('threads.posts', 'posts')
      .orderBy('category.insertedAt', 'DESC')
      .getOne()
    }
  },
  Mutation: {
    createCategory: (parent, args, {}) => {
      const argsWithSlug = { ...args, slug: SlugHelper.slugify(args.title) }
      const category = getConnection().manager.create(Category, argsWithSlug).save()
      return category
    }
  }
}

i found some code online to handle the slug generation. It replaces spaces with dashes, removes the non-word characters, and trims all the text. This will be located in a helper file. We are going to call this helper SlugHelper and it will have a method called slugify.

export const slugify = (text: string) => {
  return text
      .toString()
      .toLowerCase()
      .replace(/\s+/g, '-') // Replace spaces with -
      .replace(/[^\w\-]+/g, '') // Remove all non-word chars
      .replace(/\-\-+/g, '-') // Replace multiple - with single -
      .replace(/^-+/, '') // Trim - from start of text
      .replace(/-+$/, '') // Trim - from end of text
}
Resolvers/ThreadResolver.ts

In the ThreadResolver, we have a method to get a specific thread by passing the id, and we are doing a join to make sure this thread has posts when it gets back to the client. We have one mutation to create a thread. We are not handling if the user is logged in to create threads for now, we are just creating them, actually this is not entirely finished, because we would need the user id, which we don't have yet. We are passing just an empty string for this method. We are going to implement the authorization layer in a moment.

Threads should always have a first post, so we will need to accept the body of our post as an argument when we are creating our thread. This is just a better user experience, because what good is a thread with no first post? A thread only exists because it has posts. So, we will require the user to provide the body of the first post and the thread title when they create a thread.

import { Thread } from '../entity/Thread'
import { getConnection } from 'typeorm'
import * as SlugHelper from '../Helpers/SlugHelper'
import { Post } from '../entity/Post'

export default {
  Query: {
    thread: (parent, { id }, {}) => {
      return Thread.createQueryBuilder('thread')
      .where(`thread.id = '${id}'`)
      .leftJoinAndSelect('threads.posts', 'posts')
      .leftJoinAndSelect('thread.category', 'category')
      .leftJoinAndSelect('posts.user', 'user')
      .orderBy('posts.insertedAt', 'DESC').getOne()
    }
  },
  Mutation: {
    createThread: async (parent, args,{}) => {

      const argsWithSlug = { ...args, slug: SlugHelper.slugify(args.tigle) }
      let thread
      await getConnection().transaction(async () => {
        thread = await getConnection().manager.create(Thread, argsWithSlug)

        const argsWithUserId = { body: args.body, threadId: thread.id, userId: '' }
        await getConnection().manager.create(Post, argsWithUserId).save()
      })

      return thread
    }
  }
}
Resolvers/PostResolver.ts

On the Post Resolver, we are going to just implement the post creation. We will need the user to be authenticated so we know who’s creating the post. Since we didn't implement authentication yet, we are going to pass just an empty string as the user id for the moment, which won’t work because of the foreign key constraint. For now we’re just building towards the final code.

Once we get the post, we need to have the thread and the user included in the result, to achieve such thing, we are going to make a join between these tables and return the post with these results.

import { Post } from '../entity/Post'
import { getConnection } from 'typeorm'

export default {
  Mutation: {
    createPost: async (parent, args, {}) => {
      const argsWithUserId = { ...args, userId: '' }

      let post = await getConnection().manager.create(Post, argsWithUserId).save()

      post = await Post.findOne(post.id, {
        join: {
          alias: 'post',
          leftJoinAndSelect: {
            thread: 'post.thread',
            user: 'post.user'
          }
        },
        order:  { insertedAt: 'DESC' }
      })

      return post
    }
  }
}
Resolvers/UserResolver.ts

Our UserResolver for now will just get all the users, and create a user. The authentication will come later; for now, we will just return a token if the user exists.

To create a user, we are getting the password from args. When we set user.password = ‘something’ it sets the passwordHash field in the database based on the provided password.

import { User } from '../entity/User'
import { getConnection } from 'typeorm'

export default {
  Query: {
    users: (parent, args, {}) => {
      return User.createQueryBuilder('users')
      .leftJoinAndSelect('users.posts', 'posts')
      .orderBy('posts.insertedAt', 'DESC')
      .getMany()
    }
  },
  Mutation: {
    createUser: async (parent, args, {}) => {
      const user = await getConnection().manager.create(User, args)
      user.password = args.password
      return user.save()
    },
    authenticate: (parent, args, {}) => {
      return 'TOKEN'
    }
  }
}

We have now implemented a first pass of our Schema using our TypeORM layer. Let's see how it works by making some queries in the GraphQL Playground. We will not execute the queries that require authentication right now, since we haven’t implemented it yet. Let’s start the server and make sure things are working like we expect.

Authenticating against the GraphQL back-end

We need to authenticate our client app. There are various different ways to do this. We will handle authentication with a Bearer Token sent in the Authorization header. We’ll generate the token based on the user id and sign the token. When we get a new token back, we’ll verify the token and get the user id from it for the current user.

To achieve this, we are going to use JSON Web Tokens.

Let's add the jsonwebtoken library.

yarn add jsonwebtoken
yarn add bcrypt

In the authenticate method, we check first if the user exists by email, then compare the provided password for validity. If the password is correct, then we sign the user id information, and we return the generated token.

If the password is wrong, we throw the error WRONG_PASSWORD, and if we don't find the user we throw the error USER_NOT_FOUND.

import { User } from '../entity/User'
import { getConnection } from 'typeorm'
import * as jwt from 'jsonwebtoken'
import * as bcrypt from 'bcrypt'
import * as GraphqlErrorHandler from '../Helpers/GraphqlErrorHandler'

export default {
  Query: {
    users: (parent, args, {}) => User.find()
  },
  Mutation: {
    createUser: async (parent, args, {}) => {
      const user = getConnection().manager.create(User, args)
      user.password = args.password
      return user.save()
    },
    authenticate: async (parent, { email, password }, {}) => {
      const user = await User.findOne({ email })
      if (user) {
        const rightPassword = await bcrypt.compare(password, user.passwordHash)
        if (rightPassword) {
          const token = jwt.sign(
            {
              userId: user.id
            },
            'server secret',
            {
              expiresIn: '7d'
            }
          )
          return token
        } else {
          throw new Error(GraphqlErrorHandler.errorName.WRONG_PASSWORD)
        }
      } else {
        throw new Error(GraphqlErrorHandler.errorName.USER_NOT_FOUND)
      }
    }
  }
}

To return the right errors I read this excellent blog post, that mentions how to handle error messages and status codes in GraphQL.

We also created the GraphqlErrorHandler that returns the right error types and messages. Once we throw an error, we just pass the const name we want and we have all the messages and status code here.

export const errorName = {
  USER_NOT_FOUND: 'USER_NOT_FOUND',
  WRONG_PASSWORD: 'WRONG_PASSWORD',
  NOT_LOGGED_IN: 'NOT_LOGGED_IN'
}

export const errorType = {
  NOT_LOGGED_IN: {
    message: 'You must be logged in to perform this action.',
    statusCode: 401
  },
  USER_NOT_FOUND: {
    message: 'User was not found.',
    statusCode: 404
  },
  WRONG_PASSWORD: {
    message: 'Wrong Password.',
    statusCode: 403
  }
}

export const getErrorCode = errorName => {
  if (errorType[errorName]) {
    return errorType[errorName]
  } else {
    return errorName
  }
}

OK. We know how to send the data to the server. How does the server keep track of the user?

The Apollo Server has a tutorial on how to do it. It's called Access Control. We get the token from the headers, check if the user is correct and add this to the context.

We use the context in the resolvers, as in the example on the website.

const server = new ApolloServer({
 typeDefs,
 resolvers,
 context: ({ req }) => {
   // get the user token from the headers
   const token = req.headers.authorization || '';
  
   // try to retrieve a user with the token
   const user = getUser(token);
  
   // add the user to the context
   return { user };
 },
});

So, we can get the current user from the context in the resolvers from now on. We will implement the AuthenticationService in a moment. For now, just imagine it exists.

In the GraphQL context we will store the user identified by the token in the authorization header.

Also, as we are handling the errors, we can format the error here by using the formatError option. It helps, for instance, to not show a long stack trace in the GraphQL response.

const server = new ApolloServer({
  schema,
  context: async ({ req }) => {
    const token = req.headers.authorization
    return AuthenticationService.getUserContextFromToken(token)
  },
  formatError: err => {
    const error = GraphqlErrorHandler.getErrorCode(err.message)
    console.log(err)
    if (error.message) {
      return { message: error.message, statusCode: error.statusCode }
    }
    return { message: err.message }
  }
})

Let's implement our AuthenticationService. One of its responsibilities is to decode the token that we receive, check if the corresponding user exists, and return the user. The user will be placed into our context.

import * as jwt from 'jsonwebtoken'
import { User } from '../entity/User'

export const getUserContextFromToken = async (token: String) => {
  if (token === 'undefined' || !token) {
    return { currentUser: null }
  } else {
    let userId
    token = token.replace('Bearer ', '')

    try {
      let decoded = jwt.verify(token, 'server secret');
      userId = decoded.userId
    } catch(err) {
      // err
    }

    if (userId) {
      let currentUser: any = await User.find({ where: { id: userId } })
      if (currentUser.length > 0) {
        currentUser = currentUser[0]
        return { currentUser }
      }
    } else {
      return { currentUser: null }
    }
  }
}

export const isLoggedIn = currentUser => {
  return currentUser && currentUser.id && currentUser.email
}

The method isLoggedIn will be used later in our resolvers to make sure we have the user in our context, by checking if the user has id and email. This will be used in a moment.

Let's use this query to authenticate a request and test the mutations to create posts and threads.

mutation createUser{
  createUser(name:"Franze", username: "myUsername", email: "email@example.com", password: "password"){
    id
    insertedAt
  }
}

Now, we are going to authenticate as this user and get the token back.

mutation authentication{
  authenticate(email: "email@example.com", password: "password")
}

We can authenticate nicely. Let’s try to authenticate with a user that doesn't exist and make sure we see the error we expect:

{
  "data": {
    "authenticate": null
  },
  "errors": [
    {
      "message": "USER_NOT_FOUND",
      "locations": [
        {
          "line": 36,
          "column": 3
        }
      ],
      "path": [
        "authenticate"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [

We also have an error if we authenticate with a user and a wrong password:

{
  "data": {
    "authenticate": null
  },
  "errors": [
    {
      "message": "WRONG_PASSWORD",
      "locations": [
        {
          "line": 36,
          "column": 3
        }
      ],
      "path": [
        "authenticate"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
            "Error: WRONG_PASSWORD"

Everything seems to be working as we expect. This is working correctly. We can format the errors better on the GraphQL side. Right now, it is returning a stacktrace.

Formatting the error

Apollo Server allow us to format the errors using the formatError function in the constructor. Let's use it and we are going to use the error messages we have for our GraphQL helper.

Now our error for a wrong password.

{
  "data": {
    "authenticate": null
  },
  "errors": [
    {
      "message": "Wrong Password.",
      "statusCode": 403
    }
  ]
}

We also get a nice formatted error when the user is not found.

{
  "data": {
    "authenticate": null
  },
  "errors": [
    {
      "message": "User was not found.",
      "statusCode": 404
    }
  ]
}

Mutations that needs user

We will get the currentUser from the context. Each resolver receives as an argument: (parent, args, {}). The third parameter is the context. In our case, if the request has the right Bearer token on the authorization header, we have the currentUser in the context.

We need to create categories, threads and posts and to create those we need to have the current user in the context. We were not checking it. Now, it's time to check for it.

Let's start with the CategoryResolver. We are going to get the current user from the context, we are using the method AuthenticationService.isLoggedIn and we pass the currentUser in the context. If the user is logged in, we do what we need to do, otherwise, we throw an error for NOT_LOGGED_IN.

  Mutation: {
    createCategory: (parent, args, { currentUser }) => {
      if (AuthenticationService.isLoggedIn(currentUser)) {
        const argsWithSlug = { ...args, slug: SlugHelper.slugify(args.title) }
        const category = getConnection().manager.create(Category, argsWithSlug).save()
        return category
      } else {
        throw new Error(GraphqlErrorHandler.errorName.NOT_LOGGED_IN)
      }
    }
  }

The same thing for our ThreadResolver, we check if the user is logged in. We can now pass the correct user id, and not an empty string as we were passing.

   Mutation: {
    createThread: async (parent, args,{ currentUser }) => {
      if (AuthenticationService.isLoggedIn(currentUser)) {
        const argsWithSlug = { ...args, slug: SlugHelper.slugify(args.title) }
        let thread
        await getConnection().transaction(async () => {
          thread = await getConnection().manager.create(Thread, argsWithSlug).save()

          const argsWithUserId = { body: args.body, threadId: thread.id, userId: currentUser.id }
          await getConnection().manager.create(Post, argsWithUserId).save()
        })

        return thread
      } else {
        throw new Error(GraphqlErrorHandler.errorName.NOT_LOGGED_IN)
      }
    }
  }

And the same thing for the PostResolver. We can also pass the correct user id, and not an empty string as we were passing.

import { Post } from '../entity/Post'
import { getConnection } from 'typeorm'
import * as AuthenticationService from '../Services/AuthenticationService'
import * as GraphqlErrorHandler from '../Helpers/GraphqlErrorHandler'

export default {
  Mutation: {
    createPost: async (parent, args, { currentUser }) => {
      if (AuthenticationService.isLoggedIn(currentUser)) {
        const argsWithUserId = { ...args, userId: currentUser.id }

        let post = await getConnection().manager.create(Post, argsWithUserId).save()

        post = await Post.findOne(post.id, {
          join: {
            alias: 'post',
            leftJoinAndSelect: {
              thread: 'post.thread',
              user: 'post.user'
            }
          },
          order:  { insertedAt: 'DESC' }
        })

        return post
      } else {
        throw new Error(GraphqlErrorHandler.errorName.NOT_LOGGED_IN)
      }
    }
  }
}

Playing with the queries

Let's start by authenticating the user by using the mutation authenticate.

mutation authenticate {
  authenticate(email: "example1@example1.com", password: "password")
}

We receive a token in the result:

{
  "data": {
    "authenticate": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI5OTA3YmQ2ZS0zMzhiLTRlMzEtODVhMi1lMTllNDg2ZDg5NTciLCJpYXQiOjE1NDE1OTk3NzYsImV4cCI6MTU0MjIwNDU3Nn0.axWkcsP2eXphw-VvDZ2J7Wiz4_C4t1Y7xkwMP-FTRAk"
  }
}

And we can add this into our header.

{
  "authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI5OTA3YmQ2ZS0zMzhiLTRlMzEtODVhMi1lMTllNDg2ZDg5NTciLCJpYXQiOjE1NDE1OTk3NzYsImV4cCI6MTU0MjIwNDU3Nn0.axWkcsP2eXphw-VvDZ2J7Wiz4_C4t1Y7xkwMP-FTRAk"
}

This will make our user logged in.

Let's start by creating a category.

mutation createCategory {
  createCategory(title: "My Category Title"){
    id
    title
  }
}

Now with the id that was returned for the category we created, we can create a thread in this category.

mutation createThread{
  createThread(title: "MyThread", categoryId: "e0f525b8-78f3-4278-8413-89af5d1b990a", body: "My post body"){
    id
    title
  }
}

For this thread, we can create a post by passing the threadId.

mutation createPost {
  createPost(body:"Body of my second post", threadId: "4de44f8c-9040-492b-974f-ba80f8f50b7b") {
    id
    body
  }
}

Given our category id, we can get the list of threads, and the list of posts for its threads.

query category{
  category(id: "e0f525b8-78f3-4278-8413-89af5d1b990a"){
    threads{
      id
      title
      posts {
        id
        body
      }
    }
  }
}

Sweet!

We have our resolvers working, we can create categories, threads and posts and list all of them. All of this is interacting with the database.

Paginating categories

We will show how to do pagination on the categories type. To do this, we are going to have PaginatedCategories type, which will have a list of categories in an entries field, as well as page, perPage, totalEntries and totalPages fields.

type PaginatedCategories {
  entries: [Category]
  page: Int!
  perPage: Int!
  totalEntries: Int!
  totalPages: Int!
}

The categories query will receive the Pagination type as argument.

type Query {
  category(id: ID!): Category
  categories(pagination: Pagination): PaginatedCategories
}

The pagination type will just have page and perPage fields.

export default `

input Pagination {
  page: Int!
  perPage: Int!
}
`

So, when we are paginating the categories, we will pass page and perPage as arguments inside the pagination argument for this query.

In our resolver, we are going to receive the page and perPage arguments and use them to get the page of categories and return it.

Let's implement our categories resolver with pagination. Before going further, we are going to implement a PaginationHelper. By default, page will be 1, perPage will be 20. In the method getProperties, we are going to make the skipEelements variable, that will be used in our SQL query.

We will also have a method called getTotals, where we are going to receive entries and perPage. The getTotals function will execute getCount() on the entries. For totalPages we divide totalEntries by perPage and round up.

let PaginationHelper = {
  getProperties: function (pagination) {
    let page = 1
    let perPage = 20
    let skipElements = 0
    if (pagination) {
      page = pagination.page
      perPage = pagination.perPage
      skipElements = page * perPage
    }
    return { page, perPage, skipElements }
  },
  getTotals:  async (entries, perPage) => {
    const totalEntries = await entries.getCount()
    const totalPages = Math.ceil(totalEntries / perPage)
    return { totalEntries, totalPages }
  }
}

export default PaginationHelper

In our resolver, we are going to start by getting the arguments we have in our query for the pagination.

const { page, perPage, skipElements } = PaginationHelper.getProperties(args.pagination)

Then, we can create the query by using the TypeORM methods. We will order by insertedAt in the category. skipElements will be used on skip, and for take, we are going to use perPage.

We need to return entries, totalEntries, page, perPage, and totalPages.

We are going to get the entries by getting the result of the query builder. We will return all of this in an object that is our PaginatedCategories.

...
 categories: async (parent, args, { currentUser }) => {
      const { page, perPage, skipElements } = PaginationHelper.getProperties(args.pagination)


      const categories = Category.createQueryBuilder('category')
        .leftJoinAndSelect('category.threads', 'threads')
        .leftJoinAndSelect('threads.posts', 'posts')
        .orderBy('category.insertedAt', 'DESC')
        .skip(skipElements)
        .take(perPage)

      const { totalEntries, totalPages } = await PaginationHelper.getTotals(categories, perPage)

      return { entries: await categories.getMany(), totalEntries, page, perPage, totalPages }
    }

Let's play with the paginated categories in GraphQL Playground. We are going to pass pagination arguments specifying five entries per page, and fetching the first page:

query categories {
  categories(pagination: { page: 0, perPage: 5 }) {
    entries {
      id
      title
      slug
      insertedAt
    }
    perPage
    page
    totalEntries
    totalPages
  }
}

Let's create more categories so we can paginate them. Let's now increment the perPage field to 10. We can see it returns more categories.

We have our paginated categories! Sweet!

Adding Subscriptions

We also would like to add subscriptions to our back-end. Apollo Server has a zero-configuration setup to help us work with websockets for Apollo.

Since it’s already available, let’s just use it. In our server listen connection, we can just print the subscriptionsUrl to show the user we also have a websocket connection.

.then(connection => {
    server.listen().then(({ url, subscriptionsUrl }) => {
      console.log(`🚀  Server ready at ${url}`)
      console.log(`🚀  Websocket Server ready at ${subscriptionsUrl}`)
    })
  })

This is cool, because if we were going to use Express, we would need to spin up the websocket in another port. Apollo Server does that behind the scenes.

Category subscription

type Subscription {
  categoryAdded: Category!
}

Our category added subscription will not receive any arguments.

We are going to use the PubSub standard to send notifications when something happens. In our example, when we create a new category we notify anyone listening on categoryAdded.

We’ll implement it in the CategoryResolver.

import { PubSub } from 'graphql-subscriptions'

export const pubsub = new PubSub()

...javascript
Subscription: {
    categoryAdded: {
      resolve: (payload) => {
        return payload.category
      },
      subscribe: () => pubsub.asyncIterator('categoryAdded')
    }
  }

We will resolve the subscription categoryAdded that will return the category in the payload and it subscribes to the event categoryAdded. Let's now create our event that fires categoryAdded.

We will trigger this subscription when we create a new category. So, we publish an event with the key categoryAdded and we pass the category we've just created.

Mutation: {
    createCategory: (parent, args, { currentUser }) => {
      if (AuthenticationService.isLoggedIn(currentUser)) {
        const argsWithSlug = { ...args, slug: slugify(args.title) }
        const category = getConnection().manager.create(Category, argsWithSlug).save()
        pubsub.publish('categoryAdded', { category })
        return category
      } else {
        throw new Error(GraphqlErrorHandler.errorName.NOT_LOGGED_IN)
      }
    }
  },

This will return the category we have created.

Let's test it. To test it, let's use two instances of GraphQL Playground. One instance will be listening for the subscription, and the second will create a category.

We’ve set up the subscription for categoryAdded, and now we can create a new category.

subscription categoryAdded{
  categoryAdded{
    id
    title
  }
}

In the instance that has the subscription, we are notified about the category we just created! Sweet!

Thread subscription

Let's add subscription for new threads in a category to our schema. Our subscription receives the categoryId as an argument, because we will be listening only for newly-created threads inside of a specific category.

type Subscription {
  threadAdded(categoryId: ID!): Thread
}

Now, we will need to make our resolver for this subscription. In our mutation, we are going to publish an event key for threadAdded and we are going to pass the thread. Then when we resolve our mutation for the subscription, we are going to use the withFilter function from Apollo that lets us call the subscribe method only if the statement from the function in the second argument returns true. In our case, we are comparing the payload thread’s categoryId with the categoryId argument we passed in.

Mutation: {
    createThread: async (parent, args, { currentUser }) => {
      if (AuthenticationService.isLoggedIn(currentUser)) {
        const argsWithSlug = { ...args, slug: slugify(args.title) }
        const thread = await getConnection().manager.create(Thread, argsWithSlug).save()
        pubsub.publish('threadAdded', { thread })
        return thread
      } else {
        throw new Error(GraphqlErrorHandler.errorName.NOT_LOGGED_IN)
      }
    }
  },
Subscription: {
    threadAdded: {
      resolve: (payload) => {
        return payload.thread
      },
      subscribe: withFilter(() => pubsub.asyncIterator('threadAdded'),
      (payload, variables) => {
        return payload.thread.categoryId === variables.categoryId
      })
    }
  }

Let's now test it in Graphql Playground. Let's create a thread while listening on another instance of GraphQL Playground for new threads in the corresponding category. It works!

Post subscription

Next let’s add a subscription for posts within a thread. The subscription requires a threadId argument, because we want to listen for new posts added only inside a thread.

type Subscription {
  postAdded(threadId: ID!): Post
}

Now, we can implement our resolvers. We are going to have the post we just created, and as we want to send the post with the joined user table, we will need to join them with the user and publish to our pubsub.

Mutation: {
    createPost: async (parent, args, { currentUser }) => {
      if (AuthenticationService.isLoggedIn(currentUser)) {
        const argsWithUserId = { ...args, userId: currentUser.id }

        let post = await getConnection().manager.create(Post, argsWithUserId).save()

        post = await Post.findOne(post.id, {
          join: {
            alias: 'post',
            leftJoinAndSelect: {
              thread: 'post.thread',
              user: 'post.user'
            }
          },
          order: { insertedAt: 'DESC' }
        })
        pubsub.publish('postAdded', { post })
        return post
      } else {
        throw new Error(GraphqlErrorHandler.errorName.NOT_LOGGED_IN)
      }
    }
  },
  Subscription: {
    postAdded: {
      resolve: (payload) => {
        return payload.post
      },
      subscribe: withFilter(() => pubsub.asyncIterator('postAdded'),
        (payload, variables) => {
          return payload.post.threadId === variables.threadId
        })
    }
  }

In our subscription resolver, we are also using withFilter to check if the threadId is the same as the threadId argument we specified.

Now, we can test it. Let's listen in one instance of GraphQL Playground and then create a post on another instance. It works!

Summary

Today we saw how to use GraphQL in our back-end using JavaScript, setting up queries, mutations and subscriptions.

Links