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!
}
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!]!
}
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
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}`)
})
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.
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
}
}
In the schema we defined the object type Query as follows:
type Query {
allFruits: [Fruit!]!
}
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,
},
};
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,
});
...
Why all of this?
Let's look at it from the client perspective. The client sends a request (a query):
query ExampleQuery {
allFruits {
name
}
}
- The request wants the "name" Field for all objects in allFruits
- The Query type in the schema acts as the front door and allows for queries that want
allFruits
. It also defines that queries toallFruits
should return a non-null array of Fruit e.g.[Fruit!]!
- 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!
}
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,
},
};
Now, if a client uses the following query, it will only get an object containing a number:
query ExampleQuery {
fruitsCount
}
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
}
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),
},
};
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,
}
}
The JSON response would be:
{
"data": {
"fruitsCount": 3,
"findFruit": {
"id": "F3",
"name": "pear",
"price": 79,
"quantity": 36
}
}
}
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
}
}
The response is:
{
"data": {
"fruits": [
{ "price": 44, "name": "banana" },
{ "price": 2, "name": "blueberry" },
{ "price": 79, "name": "pear" }
]
}
}
But how do we know what we can retrieve from a GraphQL API?
In the Schema.
Top comments (0)