2 - GraphQL Core Concepts: Schema, Resolvers, Query, Apollo

lorenzojkrl

Lorenzo Zarantonello

Posted on November 23, 2023

2 - GraphQL Core Concepts: Schema, Resolvers, Query, Apollo

In this post, we will talk about the core concepts of GraphQL:

  • Schema,
  • Resolvers,
  • Query

In the next post, we will dive deeper into:

  • Mutations,
  • Subscriptions.

As said earlier, unlike REST APIs, GraphQL can use a single endpoint to fetch data from a GraphQL API.

In this sentence, you see the dual nature of GraphQL: "GraphQL is a query language for your API, and a server-side runtime for executing queries".

This is important to know because some terminology can be confusing e.g. a client queries the server is simply sending a request in a specific query format. The Query type defines the format from the GraphQL server.

In this post:

  • lowercase query defines an HTTP POST request.
  • Query with capital "C" indicates the Query type object in the GraphQL server.

As a side note, all GraphQL requests to the server are HTTP POST.

Let's see some examples to clarify the core concepts.

Schema

The schema is the rulebook that explains what is possible to retrieve from a GraphQL API. Formally, "the schema represents data objects with object types", graphql.com.

A graphQL schema is defined in a file in your backend.

For the sake of simplicity, I initialized a simple backend with npm init and I defined a schema in a file called index.js.

Here is a schema defining a Fruit.

// index.js

type Fruit {
  id: ID!
  name: String!
  quantity: Int!
  price: Int!
}
Enter fullscreen mode Exit fullscreen mode

This is just an example. But it is enough to understand that a graphQL schema roughly resembles a TypeScript type (to me at least).

To be more precise, the object type Fruit, determines that fruits can have four fields: id, name, quantity, and price.

Each field has a type (scalar type).

  • id has type ID, which is a "special" String because GraphQl makes sure it is unique.
  • name is of type String,
  • quantity is of type integer,
  • price is of type integer

In GraphQL the type of a field (e.g. scalar type) can be one of the following: String, Int, Float, Boolean, and ID.

All fields are required.

The ! symbol indicates that the field is mandatory. In more formal terms, the field should never return null. Note that an empty list [] is empty and not null.

Read more about types (object, scalar, and lists).

"To let us query anything at all, a schema requires first and foremost the Query type. [...] the Query type acts as the front door to the service", graphQL.

The Query type is a special object type (like Fruit).
So, the first Query we add is

// index.js

type Fruit {
  id: ID!
  name: String!
  quantity: Int!
  price: Int!
}

type Query {
  allFruits: [Fruit!]!
}
Enter fullscreen mode Exit fullscreen mode

Testing with Apollo Server

Since we are defining these things server-side, we can quickly test them by installing the Apollo server, as follows:

npm install @apollo/server graphql
Enter fullscreen mode Exit fullscreen mode

Once the installation is completed, change index.js to require ApolloServer. We are also including some dummy data from graphql.com.

At the bottom, we are instantiating a new ApolloServer to which we pass the schema.

Finally, we start the server with startStandaloneServer.

const { ApolloServer } = require('@apollo/server')
const { startStandaloneServer } = require('@apollo/server/standalone')

let fruits = [
  {
    "id": "F2",
    "name": "blueberry",
    "price": 2,
    "quantity": 19
  },
  {
    "id": "F3",
    "name": "pear",
    "price": 79,
    "quantity": 36
  },
  {
    "id": "F1",
    "name": "banana",
    "price": 44,
    "quantity": 84
  }
]

const typeDefs = `
  type Fruit {
    id: ID!
    name: String!
    quantity: Int!
    price: Int!
  }

  type Query {
    allFruits: [Fruit!]!
  }
`
const server = new ApolloServer({
  typeDefs,
})

startStandaloneServer(server, {
  listen: { port: 4000 },
}).then(({ url }) => {
  console.log(`Server available at ${url}`)
})

Enter fullscreen mode Exit fullscreen mode

Now, if you run node index.js you will start the GraphQL server in your terminal. You should see something like "Server available at http://localhost:4000/".

Click on http://localhost:4000/ to open Apollo Sandbox where you can test queries etc.

Apollo Sandbox

Resolver: Connecting client queries to schema

The Operation area on Apollo Sandbox allows us to try queries (query the GraphQL server) as if we would send requests from a client.

As an example, the following query (HTTP request) should return all fruit names, but it will fail.

query ExampleQuery {
  allFruits {
    name
  } 
}
Enter fullscreen mode Exit fullscreen mode

In the schema we defined the object type Query as follows:

type Query {
    allFruits: [Fruit!]!
  }
Enter fullscreen mode Exit fullscreen mode

but how do we get from the fruits array to allFruits?

We use resolvers.

Resolver Example 1

Here is one resolver:

const resolvers = {
  Query: {
    allFruits: () => fruits,
  },
};
Enter fullscreen mode Exit fullscreen mode

So our index.js becomes:

...
let fruits = [...]

const typeDefs = `
  type Fruit { ... }

  type Query {
    allFruits: [Fruit!]!
  }
`
const resolvers = {
  Query: {
    allFruits: () => fruits,
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});
...
Enter fullscreen mode Exit fullscreen mode

Why all of this?
Let's look at it from the client perspective. The client sends a request (a query):

query ExampleQuery {
  allFruits {
    name
  } 
}
Enter fullscreen mode Exit fullscreen mode
  1. The request wants the "name" Field for all objects in allFruits
  2. The Query type in the schema acts as the front door and allows for queries that want allFruits. It also defines that queries to allFruits should return a non-null array of Fruit e.g. [Fruit!]!
  3. The resolver picks up the request and returns the fruits array allFruits: () => fruits.

Resolver Example 2

Let's make another example assuming we want to get the number of fruit objects in the fruits array.

With a REST API, we would need to fetch the whole array and once we receive it, we would do something like fruits.length in the client device.

With GraphQL we can define another field in the schema type Query. The number of fruits is going to be an integer so the schema type Query becomes:

type Query {
    allFruits: [Fruit!]!,
    fruitsCount: Int!
  }
Enter fullscreen mode Exit fullscreen mode

Now we created a new "door" called fruitsCount that will return an integer, e.g. the number of fruit objects in the fruits array.

Where do we get this number? We create a resolver for the fruitsCount field.

So that when a client pings the GraphQL API with a fruitsCount query, we return the number of fruit objects in the fruits array.

Let's update the resolver as follows:

const resolvers = {
  Query: {
    fruitsCount: () => fruits.length,
    allFruits: () => fruits,
  },
};

Enter fullscreen mode Exit fullscreen mode

Now, if a client uses the following query, it will only get an object containing a number:

query ExampleQuery {
  fruitsCount
}
Enter fullscreen mode Exit fullscreen mode

Resolver Example 3

Let's assume we want to find a fruit by its name.

We start by updating the type Query (the "door") in the schema:

type Query {
    fruitsCount: Int!
    allFruits: [Fruit!]!
    findFruit(name: String!): Fruit
  }
Enter fullscreen mode Exit fullscreen mode

We then update the resolver to find and return a specific fruit

const resolvers = {
  Query: {
    fruitsCount: () => fruits.length,
    allFruits: () => fruits,
    findFruit: (root, args) => fruits.find((fruit) => fruit.name === args.name),
  },
};
Enter fullscreen mode Exit fullscreen mode

The findFruit resolver is a bit different but you can read more about resolvers here.

A client could query the GraphQL API as follows to obtain the total number of fruit objects and some specific data on the "pear" object.

query ExampleQuery {
  fruitsCount, 
  findFruit(name: "pear"){
    id,
    name, 
    price,
  }
}
Enter fullscreen mode Exit fullscreen mode

The JSON response would be:

{
  "data": {
    "fruitsCount": 3,
    "findFruit": {
      "id": "F3",
      "name": "pear",
      "price": 79,
      "quantity": 36
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Query

GraphQL uses queries to retrieve data from a GraphQL API. Each query builds on Types and Fields. In the following example (from grapql.com) we run a Query to get fruits (Type) and prices (Field):

  • We want to get data of type "fruits"
  • For each fruit we get, we want the price field

"This query syntax begins with the query keyword, followed by an operation name that describes the request's purpose—like GetFruitPrices."

query GetFruitPrices {
  fruits {
    price,
    name
  }
}
Enter fullscreen mode Exit fullscreen mode

The response is:

{
  "data": {
    "fruits": [
      { "price": 44, "name": "banana" },
      { "price": 2, "name": "blueberry" },
      { "price": 79, "name": "pear" }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

But how do we know what we can retrieve from a GraphQL API?
In the Schema.

đź’– đź’Ş đź™… đźš©
lorenzojkrl
Lorenzo Zarantonello

Posted on November 23, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related