Published on

Understanding how to expose data with GraphQL

Authors

I have been using GraphQL for some time now in the form of GatsbyJS by consuming GraphQL.

For a project, I am working on I decided to expose my endpoint as a GraphQL endpoint. My reasoning in this case:

  • Multiple Consumers: For example, you may have a mobile client and desktop client or even 2 or more desktop screens that need different fields.
    • If you were using traditional means you would have to create multiple REST endpoints to cater for this or use something like an API Gateway to expose different endpoints for different scenarios.
  • Get only Required Data: One issue with REST is you often may hit an endpoint and only use a portion of the data returned. The cool thing with GraphQL is that you can (at query time) ask for only the fields you need.
  • Combine Multiple Data Sources: If you need to query multiple endpoints for data you can easily instead use GraphQL to aggregate this for you together with the other benefits that it already provided.
    • GatsbyJS, for example, demonstrates this very well where you can install a source plugin (to consume data from a new data source) and simply update your existing queries to pull in this information.

Looking online it was a bit difficult to find a clear explanation of how data is exposed using GraphQL. The basic principle is:

Schema

You need to define a schema that details what your GraphQL data looks like.

This specifies the following things:

  • Data: What data is returned by your endpoint.
  • Data Types: What the type is of this data, for example, a name field is a String type
  • Queries: These are the read-only queries you can make against the given data type - the R part in CRUD.
  • Mutations: These are the mutable queries you can make against the given data type - the CUD part of CRUD.
scalar Date
scalar Long
type Query {
  persons: [Person!]
  personById(id: Int!): Person
  personsBySalary(salary: Long!): [Person]
}
type Mutation {
  addPerson(name: String!, surname: String!, dateOfBirth: Date!, height: Int, salary: Long!): Person

  removePerson(id: Int!): Boolean

  updateSalary(id: Int!, newSalary: Long!): Person
}
type Person {
  id: Int
  name: String
  surname: String
  dateOfBirth: Date
  height: Int
  salary: Long
}

Resolver

This is where you actually implement the code that does what you defined in your schema. I wrote the below examples in NodeJS but the principles should apply to other languages too. I used the following NodeJS dependencies in this example:

  • Apollo
    • This is an implementation of GraphQL which has a bunch of improvements and really good documentation.
  • Apollo Server Express
    • This makes exposing a GraphQL endpoint a breeze when using ExpressJS as your backend server.
  • Sequelize
    • This is an ORM for JavaScript.

For simplicity and making it easier to maintain in future, I have defined our schema and resolvers in the same file. Storing the schema together with the resolver for a given model seems like a sensible way of organizing your models.

import { gql } from 'apollo-server-express'
import Person from '../models/person'

const schema = gql`
  scalar Date
  scalar Long
  type Query {
    persons: [Person!]
    personById(id: Int!): Person
    personsBySalary(salary: Long!): [Person]
  }
  type Mutation {
    addPerson(
      name: String!
      surname: String!
      dateOfBirth: Date!
      height: Int
      salary: Long!
    ): Person

    removePerson(id: Int!): Boolean

    updateSalary(id: Int!, newSalary: Long!): Person
  }
  type Person {
    id: Int
    name: String
    surname: String
    dateOfBirth: Date
    height: Int
    salary: Long
  }
`

const resolvers = {
  Query: {
    persons: async (parent, args) => {
      return await Person.findAll()
    },
    personById: async (parent, { id }) => {
      return await Person.findByPk(id)
    },
    personBySalary: async (parent, { salary }) => {
      return await Person.findAll({
        where: { salary },
      })
    },
  },
  Mutation: {
    addPerson: async (parent, { name, surname, dateOfBirth, height, salary }) => {
      return await Person.create({ name, surname, dateOfBirth, height, salary })
    },
    removePerson: async (parent, { id }) => {
      return await Person.destroy({
        where: { id },
      })
    },
    updateSalary: async (parent, { id, salary }) => {
      return await Person.update({ salary }, { where: { id } })
    },
  },
}

module.exports = { schema, resolvers }

In our main file where we have our express server we update it as follows:

import createError from 'http-errors'
import path from 'path'
import cookieParser from 'cookie-parser'
import logger from 'morgan'
import cors from 'cors'

import express from 'express'
import { ApolloServer } from 'apollo-server-express'

import { schema, resolvers } from './graphql/orders'

const app = express()

// view engine setup
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'hbs')

const server = new ApolloServer({
  typeDefs: schema,
  resolvers,
})

server.applyMiddleware({ app, path: '/graphql' })

app.use(logger('dev'))
app.use(cors())
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(express.static(path.join(__dirname, 'public')))

// catch 404 and forward to error handler
app.use((req, res, next) => {
  next(createError(404))
})

// error handler
app.use((err, req, res) => {
  // set locals, only providing error in development
  res.locals.message = err.message
  res.locals.error = req.app.get('env') === 'development' ? err : {}

  // render the error page
  res.status(err.status || 500)
  res.render('error')
})

module.exports = app

Conclusion

We have exposed our Person DB data using Sequelize together with Apollo express. I have left out some files and setup (like the Sequelize models) but that is very easy to Google.

In general, when exposing data for GraphQL you specify the schema and code the resolvers which tell whichever GraphQL server you are using how to handle calling data of a given type, queries and mutations to that data. In our case we only hit a DB but as the resolvers are simply code nothing is stopping you from hitting other service endpoints, combining multiple data sources and getting and manipulating the data however you see fit.