This was byfar the BIGGEST drag I’ve experienced so far in the whole of FSO, and I was already dragging my feet throughout redux! Ugh. I see its usecase, but it reminds me so much of Redux. It’s an external, very abstracted, solution (library) for web apps.

What is it?

The GraphQL spec defines a query language and how a server describes its data (the schema). It’s an alternative to REST developed by Facebook. GraphQL is the API layer between the server and the client. It is a single endpoint where the client asks exactly for the data it wants, in the format the schema allows.

The main principle of GraphQL is that the client forms a query and sends it to the single GraphQL API endpoint with a HTTP POST request. This is unlike REST, as all queries are sent to the same address, and their type is POST.

It’s a specification, not a framework or library. A spec is a precise contract published by a standards body or foundation. It’s a list of rules that implementers follow so that tools built independently can work together. Like HTTP.

Now, we have the spec for GraphQl, but how do we actually implement it? With Apollo. Apollo Server is a library that allows you build a GraphQL API on Node, and Apollo Client is a library for query it from React (and its cache).

Below is an example query a client would make:

query FetchBlogsQuery {
  user(username: "mluukkai") {
    followedUsers {
      blogs {
        comments {
          user {
            blogs {
              title
            }
          }
        }
      }
    }
  }
}

Which can be intepreted as: Find a user called mluukkai, and for each of his followedUsers, go through their blogs and comments, go through the users in the comments and bring back the titles to their blogs.

The server would respond with the following JSON object:

{
  "data": {
    "followedUsers": [
      {
        "blogs": [
          {
            "comments": [
              {
                "user": {
                  "blogs": [
                    {
                      "title": "Goto considered harmful"
                    },
                    {
                      "title": "End to End Testing with Cypress is most enjoyable"
                    },
                    {
                      "title": "Navigating your transition to GraphQL"
                    },
                    {
                      "title": "From REST to GraphQL"
                    }
                  ]
                }
              }
            ]
          }
        ]
      }
    ]
  }
}

Schemas and Queries

The schema for GraphQL describes the data sent between the client and the server. Below is an example schema:

type Person {
  name: String!
  phone: String
  street: String!
  city: String!
  id: ID! 
}

type Query {
  personCount: Int!
  allPersons: [Person!]!
  findPerson(name: String!): Person
}

type Mutation {
  addPerson(
    name: String!
    phone: String
    street: String!
    city: String!
  ): Person
}

There are three types here.

  1. Person: Determines a Person type has five fields.
  2. Query: Defines what type of queries can be made to the API, and what the API is expected to return.
  3. Mutation: Defines the kind of mutations that can be made to the data in the database and what the API is expected to return.

Here’s how to read the query schema:

queryNameHere(inputVariable: variableType): returnVariable

! means it’s a required field.

Resolvers

Resolvers are functions in the server that return data (in the format that the schema defines) in response to a query. They fetch or modify the database. Apollo Server calls the appropriate function when a query or mutation request comes in.

They’re like the controllers in a REST backend, but without the attachment to multiple endpoints (since GraphQL usually POSTs to or GETs from a single endpoint) and each resolver only handles a single query/mutation/field. GraphQL handles filtering requests from the client to the right resolver and then responding in the schema-defined format.

off topic ramble about redux

Resolvers remind me a lot of the Reducers in Redux. Reducers in Redux take an action and returns an updated state in accordance to the action. Very different effects and purposes, but the in and out format feels familiar.

In Redux (man, the page got removed off the course right when I was done with it. What timing), they simplified the Flux architecture:

Flux: Action → Dispatcher → Store → View

Redux: Action → store.dispatch → Reducers → Store → View

It looks longer in the above arrow diagram, but there’s less boilerplate code since there’s no dedicated dispatcher that you need to write anymore. The store is simply configured with the reducers.


Here’s how resolvers can look like with Mongoose:

const resolvers = {
  Query: {
    personCount: () => Person.collection.countDocuments(),
    findPerson: (root, args) => Person.findOne({ name: args.name }),
    me: (root, args, context) => context.currentUser
  },
  Person: {
    phone: (root) => root.number,
    address: ({ street, city }) => {
      return {
        street: street,
        city: city
      }
    }
  },
  Mutation: {
    addPerson: async (root, args) => {
      const nameExists = await Person.exists({ name: args.name })
      if (nameExists) {
        throw new GraphQLError(`Name must be unique: ${args.name}`, {
          extensions: {
            code: 'BAD_USER_INPUT',
            invalidArgs: args.name,
          }
        })
      }
      const person = new Person({ ...args })
      return person
    }
  }
}


The Client’s Side

Apollo Client is installed on the frontend and a ApolloProvider wrapper is wrapped around the App component, passing in a client variable.

import { ApolloClient, gql, HttpLink, InMemoryCache } from '@apollo/client'
import { ApolloProvider } from '@apollo/client/react'

const client = new ApolloClient({
  link: new HttpLink({
    uri: 'http://localhost:4000',
  }),
  cache: new InMemoryCache(),
})

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </StrictMode>,
)

Queries made by the client look like the following:

const ALL_PERSONS = gql`
  query findPersonByName($nameToSearch: String!) {
    findPerson(name: $nameToSearch) {
      name
      phone 
      address {
        street
        city
      }
    }
  }
`

The name of the query is findPersonByName, and it is given a string $nameToSearch as a parameter.

And the client makes the query like so:

const [nameToSearch, setNameToSearch] = useState(null)
const result = useQuery(FIND_PERSON, {
  variables: { nameToSearch },
  skip: !nameToSearch,
})

skip means that if nameToSearch is falsy, the query is not executed.

I have decided to skip over everything else for now as I find other things more important to understand deeply over GraphQL right now.


Previous Post Previous Post