GraphQL — the Better REST
--
This article focuses on plain GraphQL functionality.
What is GraphQL?
GraphQL is a query language that provides a complete and understandable description of how to load data from a server. GraphQL enables declarative data fetching where a client can specify exactly what data it needs from an API.
GraphQL vs. REST
REST has been a popular way to expose data from a server. But as the applications became more complex, people started facing problems with the REST architectural style. Let’s consider an example of a Movie Review System to understand this more.
Suppose we have three resources — Movies, Ratings, and Reviews. Our requirement is to fetch the names of all the movies, their reviews, and the number of likes on each review. The API endpoints will be /v1/movies
, /v1/movies/reviews/{movieId}
and /v1/movies/reviews/{reviewId}
.
Problems with the above scenario
— Multiple Endpoints: In REST different resources are fetched using different endpoints. So developers have to literally maintain a list of these endpoints.
— Over-Fetching: It means that a client downloads more information than is actually required in the app. In the above example, we have required only the name of a movie but the response will contain other information like the cast of the movie or release date which is useless for the client because it only needs the name of the movie.
— Under-Fetching and n+1-requests: It means that a specific endpoint doesn’t provide enough of the required information and the client will have to make additional requests to fetch everything it needs. In the above example, we wanted to display reviews of all movies so we first fetched the movies and then their reviews by making a call to another endpoint. This caused the n+1-requests problem.
The solution to the above problems: GraphQL
GraphQL was developed to cope with the need for more flexibility and efficiency! It acts as a layer between the client and one or more data sources, receiving client requests and fetching the necessary data according to the given instructions.
Consider the above example and let’s write a GraphQL query for the given requirement.
{
movies {
movieId
name
reviews {
reviewId
likes
}
}
}
The query sent by the client specifies the shape of the data and the server responds back with the exact same shape. This solves the problem of over-fetching and under-fetching. It is not the job of the client to fetch data from different endpoints. The client just specifies the shape of the data it needs and then it is the responsibility of the server to get work done. This is how the problem of handling multiple endpoints is also solved. There is only one endpoint in GraphQL and the query is passed as a string to the server.
Another main characteristic of GraphQL is that it uses strong type system to describe data and validate queries sent by the client. GraphQL also allows gaining insightful analytics about the data that’s requested on the backend. With GraphQL, we can also do low-level performance monitoring of the requests that are processed by the server.
Core Concepts
GraphQL has three main building blocks: the schema, queries and resolvers.
Schema
The schema is a model of the data that can be fetched through the server. It defines what queries clients are allowed to make, what types of data can be fetched from the server, and what the relationships between these types are. The syntax for writing the schemas is called Schema Definition Language (SDL). Let’s consider an example of how we can use SDL to define a simple schema.
type Book {
id: Int!
title: String!
published: Date
author: Author
}
type Author {
id: Int!
name: String
books: [Book]!
}
Book
andAuthor
are of GraphQL Object Type, which means a type with some fields.id
,title
,published
,author
are fields on theBook
type. That means these are the only fields that can appear in any query that operates on Book type.Int
,String
,Date
are built-in scalar types.String!
means that fields is non-nullable, meaning that we will always get a value when we query this filed.[Book]!
represents an array of Book objects. The!
means that list in non-empty.
There are some special types within a schema:
schema {
query: Query
mutation: Mutation
subscription: Subscription
}
query
type is used to fetch the data from the database.mutation
type is used to make changes to the existing data.subscription
type is used to have a real-time connection to the server in order to get immediately informed about important events.
Every GraphQL service has a query
type and may or may not have mutation
or subscription
types. These types define the entry point of every GraphQL query. We will see in the next topic how to define entry points for queries.
Queries
GraphQL lets the client decide what data is actually needed by sending some information to the server, this information is called a query. Let’s take a look at an example query that a client can send to the server.
{
author {
name
}
}
Response would look like
{
"data": {
"author": [
{ "name": "William Shakespeare" },
{ "name": "Siddhartha Mukherjee" },
...
]
}
}
You can see immediately that the query has exactly the same shape as the result. This means that we always get back what we expect and the server also knows what the client is asking for.
This query will be executed if there is an entry point defined for it in a schema. Let’s define an entry point for the above query.
type Query {
author: [Author]
}
In query, fields can also refer to Objects. In that case we can make sub-selection of fields for that Object. Let’s look at an example for such case.
{
author {
name
book {
title
}
}
}
Response would look like:
{
"data": {
"author": [
{
"name": "William Shakespeare",
"books": [
{ "title": "The Tragedy of Macbeth" },
{ "title": "Measure for Measure" },
...
]
},
...
]
}
}
GraphQL also provides the ability to pass arguments to the fields. For example:
{
author(id: "101") {
name
}
}
Response would look like:
{
"data": {
"author": {
"name": "Siddhartha Mukherjee"
}
}
}
In GraphQL, every field and nested object can get its own set of arguments, making GraphQL a complete replacement for making multiple API fetches.
Up until now, we have been using a shorthand syntax where we omit both the query
keyword and the query name, but it's useful to use these to make our code less ambiguous. Let’s look at an example that includes the keyword query
as operation type and AuthorNameAndBooksTitle
as operation name.
query AuthorNameAndBooksTitle {
author(id: "101") {
name
book {
title
}
}
}
- The operation type is either query, mutation, or subscription and describes what type of operation you’re intending to do.
- The operation name is a meaningful and explicit name for your operation.
In the above example we have passed the value of id
inside the query string. But it is not a good idea to pass these dynamic arguments directly in the query string. Instead, GraphQL provides a better way to factor dynamic values out of the query, and pass them separately. These values are called variables. Let’s look at an example:
query AuthorNameAndBooksTitle($id: Int) {
author(id: $id) {
name
book {
title
}
}
}
This is a good practice for denoting which arguments in our query are expected to be dynamic.
Resolvers
Now that we have defined the structure of a GraphQL server — its schema, next comes the concrete implementation that determines the server’s behavior. Key components for the implementation are called resolver functions.
The sole purpose of a resolver function is to fetch the data for its field. In the GraphQL server implementation, each field corresponds to exactly one resolver function. So when the server receives the query, it will call all the functions for the fields that are specified in the query’s payload. It thus resolves the query and retrieve the correct data for each field.
GraphQL resolve functions can contain arbitrary code, which means GraphQL server can talk to any kind of backend, even other GraphQL servers. For example, the Author type can be stored in a SQL Database and the Book type can be stored in MongoDB, or even handled by a microservice.
Let’s write a resolve functions for our above schema with types Author and Book.
Query: {
book(root, args, context, info) {
return fetchBookById(args.id);
}
}
root
argument in each resolver call is simply the result of the previous call and initial value isnull
if not otherwise specified.args
argument carries the parameters for the query.context
argument is provided to every resolver and holds important contextual information like access to a database.info
argument holds field specific information relevant to the current query and the schema.
Assuming a function fetchBookById
is defined and returns a instance of Book
, the resolve function enables the execution of the schema.
Here we are putting resolver on Query
because we want to query for book
directly on the root level. But we can also have resolvers for sub-fields such as a book
’s author
field:
Book: {
author(book) {
return fetchAuthorByBook(book); //hit the database
}
}
Resolvers can also be used to modify the contents of a database and in that case they are known an mutation resolvers.
Query Execution
At a high-level view the server responds to the query in three steps
- Parsing the query
First, the server parses the string and turns it into Abstract Syntax Tree. If there are any syntax error the execution will be terminated. - Validation
This stage makes sure that the query is valid given the schema before the execution starts. - Execution
After being validated, a GraphQL query is executed by a GraphQL server which returns a result that mirrors the shape of the requested query.
Let’s write the execution flow for the above mentioned query for fetching the author
and its book
.
1: run Query.author
2: run Author.name and Author.books (for Author returned in 1)
3: run Book.title (for Book returned in 2)
Conclusion
GraphQL might seem complex at first because it is an API technology and it can be used in any context where an API is used. But as you can see, once we dive into it GraphQL is pretty easy to understand.
In most cases, building an app with GraphQL is better choice than REST since it delivers a standard for one-trip relational queries rather than multiple round trip queries, reduces the amount of code to write, is less error prone and provides built-in documentation.
GraphQL’s standardized architecture is ultimately better, cheaper and faster than the REST alternative.
References
[1] https://www.howtographql.com/
[2] https://graphql.org/learn/
[3] https://www.apollographql.com/docs/