Have you heard about GraphQL? Facebook open-sourced it in 2015 and I believe it is a much better way to establish contracts between a client and a server than REST. If, like me, you are always searching for the easiest way to develop and maintain code, this GraphQL Tutorial is for you.
In this article we will:
- Give a brief introduction to GraphQL
- Show the improvements of GraphQL over REST
- Show you, step by step, how to add a feature to a GraphQL app written in PHP
It’s worth noting that both the code and the application we are going to explore are open source, so you can use it as a foundation for your own work. With such new technologies, ‘best practices’ are constantly evolving. As a result, I have thoroughly researched and refactored all code used in this article and hope it can serve as example code to get you started on your GraphQL journey.
Note: I use the term ‘information’ and ‘informations’ throughout this article to describe, respectively, the singular and plural of an object. This stems from the article originally being written in Portuguese, where it makes sense to use the plural for the word ‘information’.
How I ended up testing GraphQL
I have been using React as my frontend for a while and was mostly happy with its performance. My apps became a lot cleaner thanks to React’s paradigm. Its beauty lies in the fact that you always have a predictable view for a specific model state. The headaches caused by most bind and listeners are not present, which results in clean and predictable code.
React focuses on rendering the view: having the app state as the starting point and as the single source of truth. To manage that app state I was using the Redux library. Since I started using these two libraries together, there has not been a single day where I’ve lost time tracking the cause of any unexpected view behaviors.
Even with all the above benefits, there was a fundamental problem with something out of the scope of these libraries: data integration with the server. Specifically, asynchronous data integration.
This is why I was looking for a way to integrate data into my React frontend. I tried Relay, but stopped, due to their lack of documentation at that time. I then came across the GraphQL client Apollo JS.
React Apollo Client
Apollo Client is an efficient data client that runs alongside your JS application and is responsible for managing the data and data integration with the server.
It comes loaded with features like queries, caching, mutations, optimistic UI, subscriptions, pagination, server-side rendering, and prefetching. What’s more: Apollo also benefits from an active and enthusiastic community supporting the product with full documentation and very active Slack channels.
The client does the ‘dirty work’ of making asynchronous queries to the server very well, normalizing the data received and saving that in a cache. It also takes care of rebuilding the data graph from the cache layer and injecting it to the view components (using high order components, a.k.a. HOCs). The syncing between server and client is so efficient that you will see areas on the screen that you did not expect updating to their correct values. That’s a huge time saver on the frontend.
Apollo is also able to issue mutations to the server using GraphQL, since communication with the server is taken care of by Apollo. This allows developers to concentrate more time on the view and business logic.
An Introduction to GraphQL
GraphQL puts a lot of control onto the client, allowing it to make queries that specify the fields it wants to see and as well as the relations it wants. This reduces requests to the server dramatically.
The introspective aspect of the language allows validation of the request payloads and responses in the GraphQL layer. This imposes a good contract between client and server. It further allows for custom development tools to build great API documentation with very little human interaction. Indeed, all we need is to add description fields to the schema.
While building the server, I also found out that the GraphQL model imposes an elegant server architecture, allowing focus to be more on the business logic and less on the boilerplate code.
GraphQL has been evolving so fast that, since its recent inception, it already has implementations for JS, Ruby, Python, Scala, Java, Clojure, GO, PHP, C#, .NET and other languages. It also has a rapidly growing ecosystem, with serverless services regularly popping up offering to use it as an interface.
The broader development community has been waiting a long time for a solution to replace the outdated REST. Modern applications are a lot more client-oriented because it makes a lot of sense to give clients more responsibility and flexibility on how they query server information.
I believe that the origins of GraphQL, within a large corporate entity, have given it an advantage. It means it has been rigorously tested in ‘real life’ scenarios (as opposed to academic ones) before being opened up to the developer community. This all contributes to its robustness and excellent fit with modern apps.
GraphQL improvements over REST
A good way to see the advantages of GraphQL is to see it in practice. Imagine, for example, that you want to retrieve a list of posts from a user using REST. You would probably access and endpoint as per the below query:
>http://mydoma.in/api/user/45/posts
What information does that give us? What data will come from that request? We can’t know unless we look at the code or add documentation over it, using a tool like Swagger.
Now, let’s look at the same query in GraphQL:
GraphQL querying the posts field of the Query “namespace” with an operation named “UserPosts”
query UserPosts{
posts(userId:45) {
id
title
text
createdAt
updatedAt
}
}
Takeaway: GraphQL usually has only one endpoint and we make all requests within it with queries like this.
To find out what queries are available, we define a schema on the server. The server defines a schema that lists the queries, possible arguments and return types.
The schema for our example above would look something like this:
# Queries.types.yml
# ...
fields:
posts:
type: "[Post]"
args:
userId:
type: "ID!"
resolve: "@=service('app.resolver.posts').find(args)"
# Post.types.yml
Post:
type: object
config:
description: "An article posted in the blog."
fields:
id:
type: "ID!"
title:
type: "String"
text:
type: "String"
description: "The article's content"
createdAt:
type: "DateTime"
updatedAt:
type: "DateTime"
comments:
type: "[Comment]"
So this schema has a lot to declare:
- It declares the query “posts”
- That this query returns an array of objects of the type “Post”
- That the query can accept a “userId” argument of type ID that is required (!)
- How to resolve that query. In the example, a service method (find) is called with the passed args
- It also declares the Post type, with strongly-typed fields
As you can see, it’s similar to a Swagger file. The difference is that part of our development process is to declare that schema. And by declaring that we are also writing our docs. This is a big improvement over REST.
Takeaway: Documentation generated within the process is far more reliable for ‘up-to-dateness’.
Let’s imagine now that we need to access this same service from a mobile device to make a simpler list of all posts. We just need two fields: title and id.
How would we do that in REST? Well, you would need to pass a parameter to specify this return type and code it inside our controller. In GraphQL, all we need to update is a query to say what fields are needed.
This is a query to the posts field in the query namespace. It defines the userId argument and the fields it will need: id and title
query UserPosts{
posts(userId:45) {
id
title
}
}
This query will return an array of posts with only two fields: id and title. So we can now see another improvement over REST.
Takeaway: In GraphQL the client can specify in the query what fields are needed – so only those are returned in the response. This saves us the need to write another endpoint.
Let’s consider another scenario where we look at a list of articles and want the last 5 comments for each of them. In REST, we could do something like the below:
Requesting the posts of user 45 using REST
http://mydoma.in/api/user/45/posts
and then request each post to grab the comment list for all posts returned
http://mydoma.in/api/post/3/comments?last=5
http://mydoma.in/api/post/4/comments?last=5
http://mydoma.in/api/post/7/comments?last=5
Or we could develop a custom “impure” REST endpoint.
http://mydoma.in/api/user/45/postsWithComments?lastComments=5
Or we could add the parameter with=comments to tell that endpoint to nest the comments into the posts. This would require changes to the service.
http://mydoma.in/api/user/45/posts?with=comments&lastComments=5
As you can see, these are cumbersome solutions. Let’s now look at how we would do this in GraphQL:
Querying the posts field in the query namespace. We pass the argument userId=45 to say we want the posts of that user. We also pass all the required fields (id,title,text,…). One of these fields, comments is an object field, and that tells GraphQL to include that relation on the response. Please, notice that even this field (that is an edge or relation) receives its argument (last=5)
query UserPosts{
posts(userId:45) {
id
title
text
createdAt
updatedAt
comments(last:5) {
id
text
user {
id
name
}
}
}
}
That’s all that was needed was to add “comments” to my query.
In a GraphQL query, you can nest the fields you need, even if they are relations.
I also declared, in the parentheses after the comments field, that I needed the last 5 comments there. The naming is clearer because I did not need to come up with a weird ‘lastComments’ argument name.
You are even able to pass arguments to every field or relation inside a query.
The GraphQL query is a lot easier to understand than the cumbersome REST calls. We can look at it and see that we want the posts from user 45 as well as the last 5 comments of each post with the id and name of the user of each comment. Simple.
It is also very easy to implement server side. The posts resolver there does not need to know we want the post WITH the comments. This is because the comments relation has its own resolver and the GraphQL layer will call it on demand. In short, we don’t need to do anything different to return the post than when we return it nested with its comments.
With GraphQL the server code is clean and well-organized.
Hopefully, this section has given you a clear understanding of why I consider GraphQL more efficient that REST.
In the following sections, where we look at some real world examples, and you will start to understand some of the deeper intricacies of GraphQL.
strong>GraphQL Tutorial
I will guide you through the following examples in the same order I would follow when developing a new feature for an app.
About the example app
All the following examples are from a Knowledge Management App. The main purpose of this app is to organize knowledge that is shared through Telegram.
The main screen, as shown below, is where we organize messages in specific threads or conversations and put tags on those threads.
The following examples will be simple, as our goal is just to explain the overall architecture.
If you are interested in diving deeper, this App has some more complex data structures you could study. Including:
- A paginated (infinite scroll) list – Messages
- A non-paginated list – Threads
- A tree – Subtopics (not shown in the gif)
- Lots of simple views, menus and buttons.
- Forms and data editing
Everything can be found in the repository.
Installing the Code on Your Machine
All code and images are available in this article. But, I encourage you to clone the repo and look at the rest of the code.
- Clone the backend repo.
- Run composer install and configure your parameters
- Import Fixtures and Data
- Start the PHP server
- Familiarize yourself with GraphiQL, a graphical interactive in-browser GraphQL IDE.
Development Cycle
These are the steps I usually take when I add any new functionality to a system:
- Define the functionality
- Define the schema
- Write test(s)
- Make test pass – Improving the schema and the resolver
- Refactor
Defining the New Functionality
We are going to add extra information to the classification (tagging) of a thread in a specific subtopic.
In other words, we will be changing this:
Into this:
In GraphQL, you can see the documentation of the server and also run queries and mutations in it. If you started your own server, it should be running under 127.0.0.1:8000/graphiql.
Let’s look at the mutation that is used to insert ‘Information’ (which is an entity in our system). It’s the relationship between a Thread and a Subtopic. You can also understand it as a tag.
The mutation is called “informationRegisterForThread”.
Below is the schema of that mutation field, as seen in the GraphQL tool. This is auto-generated and available as soon as you write the schema. Since writing the schema is a main part of the development process, you will always have up-to-date documentation on your API.
You can see the mutation expects a required id (ID!) and also an InformationInput object. If you click on InformationInput, you will see its schema:
Note: I like putting the noun before the verb in order to aggregate mutations in the docs. That’s a workaround I’ve found due to the non-nested characteristics of GraphQL mutations.
It might seem unnecessary to have a nested InformationInput object into those arguments because it now contains only one subtopicId field. It is, however, good practice when designing a mutation because you reserve names for future expansion of the schema and also simplify the API on the client.
And this will help us now because we need to add a new input field to that mutation, to register that extra ‘about’ text. We can add that field inside our InformationInput object. So let’s start by changing our schema:
Defining Schema
Below is the InformationInput type definition in our schema. We are going to add the ‘about’ field to it. It’s a String and is optional. It would be required if it was “String!”. We also refine the descriptions here. input-object is a specific object type to receive data.
#InformationInput.types.yml
# from ...
InformationInput:
type: input-object
config:
fields:
subtopicId:
type: "ID!"
# to ...
InformationInput:
type: input-object
config:
description: "This represents the information added to classify or tag a thread"
fields:
about:
type: "String"
description: 'Extra information beyond the subtopic'
subtopicId:
type: "ID!"
description: 'The subtopic that represents where in the knowledge tree we classify the thread.'
We have now added the ‘about’ field and improved the documentation for the ‘description’ fields. Let’s look at these now:
If you click on ‘about’ and ‘subtopicId’, you will be able to read the descriptions added for those fields. We are writing our app and writing our API docs at the same time in the exact same place.
Now we may add a new field ‘about’ when calling our mutation. Since our new field is not mandatory, our app should still be running just fine.
Our schema is created! Before we actually implement the saving of that data, let’s do some Test-Driven Development (TDD). The schema is easily visible on the frontend and it’s ok to write it without testing. The resolver action should be tested though, so let’s look at that.
Writing Tests
Most of my tests run against the GraphQL layer because, this way, they also test the schema. When incorrect data is sent, errors are returned. To run the test correctly, you should clear the cache every time.
bin/console cache:clear --env=test;phpunit tests/AppBundle/GraphQL/Informations/Mutations/InformationRegisterForThreadTest.php
Let’s look at the test we have in place. This test does not require a fixture. It creates all its required data: two subtopics (tags) and 3 threads. The createThread will create dummy messages and add them to threads. After that, it will add information to the thread. Information is the relation between a thread and a subtopic (a.k.a. tag).
After that, it will read the thread’s information objects and assert that two such objects were inserted into the thread with id $t1. It will also make some other assertions.
The upper case methods are the ones that will make direct calls to the GraphQL queries and mutations.
# TestsAppBundleGraphQLInformationsMutationsInformationRegisterForThreadTest
function helper() {
return new InformationTestHelper($this);
}
/** @test */
public function shouldSaveSubtopicId()
{
$h = $this->helper();
$s1 = $h->SUBTOPICS_REGISTER_FIRST_LEVEL(['name'=>"Planta"])('0.subtopics.0.id');
$s2 = $h->SUBTOPICS_REGISTER_FIRST_LEVEL(['name'=>"Construcao"])('0.subtopics.1.id');
$t1 = $h->createThread();
$t2 = $h->createThread();
$t3 = $h->createThread();
$h->INFORMATION_REGISTER_FOR_THREAD([
'threadId'=>$t1,
'information'=>['subtopicId'=>$s1]
]);
$h->INFORMATION_REGISTER_FOR_THREAD([
'threadId'=>$t1,
'information'=>['subtopicId'=>$s2]
]);
$h->INFORMATION_REGISTER_FOR_THREAD([
'threadId'=>$t3,
'information'=>['subtopicId'=>$s2]
]);
$informations = $h->THREAD(['id'=>$t1])('thread.informations');
$this->assertCount(2,$informations);
$this->assertEquals($s1,$informations[0]['subtopic']['id']);
$this->assertEquals($s2,$informations[1]['subtopic']['id']);
$informations = $h->THREAD(['id'=>$t2])('thread.informations');
$this->assertCount(0,$informations);
$informations = $h->THREAD(['id'=>$t3])('thread.informations');
$this->assertCount(1,$informations);
$this->assertEquals($s2,$informations[0]['subtopic']['id']);
}
*/
Let’s write our test to add the ‘about’ data into our query and see if its value is returned back when we read the thread.
The helper (InformationTestHelper) is responsible for calling queries on the GraphQL layer and returning a function. It returns a function so that we can call it with a json path to grab what we need. This pattern, function returning a function, may seem a little tricky at first, but it is worth using because of the clarity we get from its output.
If we refactor a little, you will see what I’m talking about:
The fixture creation is refactored into createThreadsAndSubtopics. The call to:
THREAD, with id=$t1 (…$h->THREAD([‘id’=>$t1]..)
returns a function that we call again passing 3 path strings
(‘thread.informations’, ‘thread.informations.0.subtopic.id’, ‘thread.informations.1.subtopic.id’)
That will return those 3 values as an array that we then assign to $informations, $s1ReadId and $s2ReadId using the list function.
/** @test */
function createThreadsAndSubtopics() {
$h = $this->helper();
$s1 = $h->SUBTOPICS_REGISTER_FIRST_LEVEL(['name'=>"Planta"])('0.subtopics.0.id');
$s2 = $h->SUBTOPICS_REGISTER_FIRST_LEVEL(['name'=>"Construcao"])('0.subtopics.1.id');
$t1 = $h->createThread();
$t2 = $h->createThread();
$t3 = $h->createThread();
return [$s1,$s2,$t1,$t2,$t3];
}
public function shouldSaveSubtopicId()
{
$h = $this->helper();
list($s1,$s2,$t1,$t2,$t3) = $this->createThreadsAndSubtopics();
$h->INFORMATION_REGISTER_FOR_THREAD([
'threadId'=>$t1,
'information'=>['subtopicId'=>$s1]
]);
$h->INFORMATION_REGISTER_FOR_THREAD([
'threadId'=>$t1,
'information'=>['subtopicId'=>$s2]
]);
$h->INFORMATION_REGISTER_FOR_THREAD([
'threadId'=>$t3,
'information'=>['subtopicId'=>$s2]
]);
list(
$informations,
$s1ReadId,
$s2ReadId
) = $h->THREAD([
'id'=>$t1
])(
'thread.informations',
'thread.informations.0.subtopic.id',
'thread.informations.1.subtopic.id'
);
$this->assertCount( 2 , $informations );
$this->assertEquals( $s1 , $s1ReadId );
$this->assertEquals( $s2 , $s2ReadId );
$this->assertCount(
0,
$h->THREAD(['id'=>$t2])('thread.informations')
);
$informations = $h->THREAD(['id'=>$t3])('thread.informations');
$this->assertCount(1,$informations);
$this->assertEquals($s2,$informations[0]['subtopic']['id']);
}
So this:
$informations[1]['subtopic']['id']
Is returned as:
$s2ReadId
In response to the query:
'thread.informations.1.subtopic.id'
That query path was run by JsonPath into the response that came from the GraphQL layer. This happens on the second function call. Functions returning functions is not a usual pattern seen in PHP, but it’s a very powerful tool, used a lot in functional programming.
This might be a little confusing if it’s the first time you are looking at this pattern. So I encourage you to explore the test helper code to understand what is going on.
Now let’s test for the new field we are going to add. We will start by registering an INFORMATION object with data in the ‘about’ field. After that, we will load that thread back, query that field’s value and assert it is equal to the original string.
In this test, we will use the createThreadsAndSubtopics function to create some data. After that, we will call the informationRegisterForThread field in the mutation object defined in our schema. We do that using the INFORMATION_REGISTER_FOR_THREAD helper. In that call we pass two arguments: the threadId and the InformationInput object we just defined with the ‘about’ field. After that, we query the thread and use the path ‘thread.informations.0.about’ to grab the value of the ‘about’ field. We then check to see if the value was saved.
# TestsAppBundleGraphQLInformationsMutationsInformationRegisterForThreadTest
/** @test */
public function shouldSaveAbout()
{
$h = $this->helper();
list($s1,$s2,$t1) = $this->createThreadsAndSubtopics();
$h->INFORMATION_REGISTER_FOR_THREAD([
'threadId'=>$t1,
'information'=>[ # information field
'subtopicId'=>$s1,
'about'=>'Nice information about that thing.' # about field
]
]);
$savedText = $h->THREAD(['id'=>$t1])( # query for the thread
'thread.informations.0.about' # query the result for that field
);
$this->assertEquals( 'Nice information about that thing.' , $savedText ); # check it
}
Take a breath
Let’s recap a little. We first defined the functionality by making our user interface and by defining the input schema. After that we wrote a test saving and requesting the about field.
We still need to add that field in the query, persist that value on the db and add it to the response. Please notice we have changed the input schema, but not the query schema.
Now we will continue in a test driven way, solving the issues presented by the tests. It might seem a little counterintuitive to those not used to it, but in fact, it’s a powerful way to code, because you can be sure you are also adding useful code.
Passing The Test
If we run this test straight away, it will fail, saying that it “could not query the about field” on the response returned by the THREAD query’. It is requested by the path (‘thread.informations.0.about’), requesting for the about field on the first (index 0) information object on the thread.
Requesting the about field
So, we need to request the about field on that query. Please remember we added that field to the input server schema, making it possible to be inserted. But we did not actually make it available to be queried. Nor was it requested it on the frontend query. To request it, we need to go into the THREAD query helper. This makes the call to the thread field in the query namespace and adds the ‘about’ field.
This code is also nice because you can see how the helper is written and how the thread query is written. Please notice that the processResponse method will return a function that can be called with paths to query the response data.
# TestsAppBundleGraphQLTestHelper.php
function THREAD($args = [],$getError = false, $initialPath = '') {
$q ='query thread($id:ID!){
thread(id:$id){
id
messages{
id
text
}
informations{
id
about # <-- ADDED THIS FIELD subtopic{ id } } } }'; return $this->runner->processResponse($q,$args,$getError,$initialPath);
}
After adding that, we will get this error:
[message] => Cannot query field "about" on type "Information".
And that’s correct because we are requesting the ‘about’ field, but it does not exist in the Information type. We added ‘about’ only to the InformationInput type to receive that data.
Now we need to add it to the Information GraphQL type.
#Information.types.yml
Information:
type: object
config:
description: "A tag, a classification of some thread, creating a relationship with a subtopic that indicates a specific subject."
fields:
id:
type: "ID!"
thread:
type: "Thread"
description: "Thread that receive this information tag"
subtopic:
type: "Subtopic"
description: "The related subtopic"
about:
type: "String"
description: "extra information"
Now that Information type on the server has the ‘about’ field, we run the test again and get the following message:
Failed asserting that null matches expected 'Nice information about that thing.'.
From this, we can see that the GraphQL is ok because we did not get any validation errors from the data being sent or retrieved back.
The error we received is because, in fact, we have not yet persisted our data to the DB. So, let’s work on the resolvers now.
Persist the about field to the DB
We will add the field to our mutation resolver. And also add the field to our ORM as shown in the code below by the annotation.
# 1 - AppBundleResolverInformationsResolver
public function registerForThread($args)
{
$thread = $this->repo('AppBundle:Thread')->find( $args['threadId'] );
$subtopic = $this->repo('AppBundle:Subtopic')->find( $args['information']['subtopicId'] );
$about = $args['information']['about']; # here
$info = new Information();
$info->setSubtopic($subtopic);
$info->setAbout($about); # and here
$thread->addInformation($info);
$this->em->persist($info);
$this->em->flush();
$this->em->refresh($thread);
return $thread;
}
#AppBundleEntityInformation
@ORMColumn(name="about", type="text", nullable=true)
If we run the test again we get a ‘test successful’ message. We’ve done it!
Note: I encourage you to open SubtopicsTestHelper and follow the ‘proccessResponse’ method (ReisGraphQLTestRunnerRunnerRunner::processGraphQL). There you will be able to see the GraphQL call happening and the json path component being wrapped in the returning function.
This GraphQL article from DeBergalis is a good reference for this type of development.
So, what have we done so far?
- Understood our functionality
- Defined the schema
- Wrote our test
- Wrote the resolver
- Updated Doctrine
Each step logically follows the next and could also be written by what they achieve:
- Know what you are doing
- Define the contract and put validation in place
- Define the expected behavior
- Implement the logic
- Implement the persistence
Other Uses For Mutations
Currently, our test runs the informationRegisterForThread mutation and then uses the thread query to check for the inserted data. But, mutations can also return data. In fact, if we look carefully at them, mutations are identical to other queries. They have a return type and can specify the format of the returned data.
If we were making these calls from the client, we would be doing two queries: one for the mutation and another to retrieve the thread again. That’s why the return type of the mutation is Thread:
The mutation is also a query on the thread. So, instead of calling the mutation and then calling the thread query, as per below….
$h->INFORMATION_REGISTER_FOR_THREAD([
'threadId'=>$t1,
'information'=>[ # information field
'subtopicId'=>$s1,
'about'=>'Nice information about that thing.' # about field
]
]);
$savedText = $h->THREAD(['id'=>$t1])( # query for the thread
'thread.informations.0.about' # query the result for that field
);
$this->assertEquals( 'Nice information about that thing.' , $savedText ); # check it
We use the thread returned by the mutation and query it’s return (json path) with the ‘informations.0.about’. See below.
list($savedAbout) = $h->INFORMATION_REGISTER_FOR_THREAD([
'threadId'=>$t1,
'information'=>[
'subtopicId'=>$s1,
'about'=>'Nice information about that thing.'
]
])('informations.0.about');
$this->assertEquals( 'Nice information about that thing.' , $savedAbout );
This will register the mutation and return the information required to update the client with only one request. With this method, even our test become cleaner!
Since the thread has an information collection, it could be any of the information in the array. So, how can we know which? We could return the information instead of returning the thread. But, that’s unnecessary as we could have some changes on the thread. In fact, we can create specific types for our mutation returns.
Creating a Specific Type to Our Mutation’s Result
I was reading this article on how to design mutations and it enforces the argument that a mutation should always make its own type to return. This makes a lot of sense because by doing this, we create a schema that’s more flexible and has a good extension point. Adding a field to a return type, like InformationRegisterForThreatReturn, is a lot easier than changing a mutation return type.
In our case, that would allow us to return the informationId and the thread. Once we do this we have:
#Mutation.types.yml
informationRegisterForThread:
type: "Thread"
args:
threadId:
type: "ID!"
information:
type: "InformationInput"
resolve: "@=service('app.resolver.informations').registerForThread(args)"
The result type is the ‘Thread’ type. This gives us almost no flexibility to add or remove ‘informations’ that might be needed there. So let’s create a specific return type for this mutation. This type is not a system entity representation, but is a representation of a response of this mutation. That gives us room to add extra information to this response without breaking a lot of code or the mutation contract. :
Note: Create a specific mutation return type for every mutation. That will give you flexibility on your schema evolution.
# Mutation.types.yml - at the first level, after Mutation root field
InformationRegisterForThreadResult:
type: object
config:
fields:
thread:
type: "Thread"
And change this type in our mutation:
#Mutation.types.yml
informationRegisterForThread:
type: "InformationRegisterForThreadResult"
...
Now we have added a layer of flexibility to our return. Let’s say we want to know the informationId of the information that was just inserted. We can do that easily by adding a new field to the mutation result. That won’t break anything on the client, due to the object returned. If the return was still the ‘Thread’ type, that would not be the case.
# Mutation.types.yml - at the first level, after Mutation root field
InformationRegisterForThreadResult:
type: object
config:
fields:
thread:
type: "Thread"
informationId:
type: "ID"
OK, we’re done. We’ve added some simple functionality to our system.
Components Overview
Here are the big components we used in the architecture for this app:
The GraphQL Layer, implemented using the OverblogGraphQLBundle.
- We define the GraphQL schema and expose an endpoint.
- All queries and mutations will enter through that endpoint.
- Validation will be run, using a strict type system.
- Execution will run through resolvers.
The Testing Layer.
- Ok, I know tests are not strictly a layer on the architecture. I want to leave them here to remind you that you can call those GraphQL queries using the tests and implement a safety net for your system. Tests can also serve as documentation.
The Resolvers Layer, implementing business logic:
- These are simple PHP classes registered as services
- We map them using the Expression Language in the schema definition.
- Resolvers receive validated arguments and return data.
- Resolvers alter data when running mutations.
- Returned data is also validated by the GraphQL layer on its way back.
- Usually, resolvers will call a data layer like Doctrine to do their job.
- Resolvers can also call a lot of different services, even a REST call can be made.
- The Data Layer, specific to your application.
This next section should be used as a reference if you want to try this yourself.
Reference
To build the GraphQL server with Symfony, I used the Overblog GraphQL Bundle. It integrates the Webonyx GraphQL lib into a Symfony bundle, adding nice features to it.
One very special feature it has is the Expression Language. You can use it to declare the resolvers, specifying what service to use, what method to call and what args to pass like in the string below.
# Mutation.types.yml
resolve: "@=service('app.resolver.informations').registerForThread(args)"
The bundle also implements endpoints, improves the error handling and adds some other things tp the Webonyx GraphQL lib. One nice lib it requires is the overblog/graphql-php-generator that is responsible for reading a nice and clean yml file and converting it into the GraphQL type objects. This bundle adds usability and readability to the GraphQL lib and was, along with the Expression Language, the reason I decided to migrate from Youshido‘s lib.
Schema Declaration
Our schema declaration is the entrance to the backend. It defines the API interface for the outer world.
It is located in src/AppBundle/Resources/config/graphql/ and queries are defined in the Query.types.yml and mutations are in Mutations.types.yml.
The resolvers are nicely defined there, along with the expression language we’ve talked about. To fulfill a request, more than one resolver can be called in the schema tree.
Disabling Cors to Test on Localhost
This app is not on a production server yet. To test it on localhost, I usually run the client on one port and the server on another, and I got some cors verification errors, so I installed the NelmioCorsBundle to allow that.
The configurations are very open and will need to be a lot more stringent on a prod server. I just wanted you to note that it’s running and will help you to avoid a lot of errors seen on the frontend client. If you don’t know this bundle yet, it’s worth taking a look at. It will manage the headers sent and especially the OPTIONS pre-flight requests to enable cross-origin resource sharing. In other words, will enable you to call your API from a different domain.
Error Handling
Error handling is a very open topic in the GraphQL world. The specs don’t say a lot (1,2) and are open to interpretation. As a result, there are a lot of different opinions on how to handle them (1,2)
Because GraphQL is so recent, there are no well-established best practices for it yet. Take this into consideration when designing your system. Maybe you can find a more efficient way of doing things. If so, test it and share with the community. Overblog deals with errors in a manner that is good enough for me. First, it will add normal validation errors when it encounters them during the validation of input or output types.
Second, it will handle the exceptions thrown in the resolvers. When it catches an exception, it will add an error to the response. Almost all exceptions are added as an “internal server error” generic message. The only two Exception types (and subtypes) that are not translated to this generic message are:
ErrorHandler::DEFAULT_USER_WARNING_CLASS
ErrorHandler::DEFAULT_USER_ERROR_CLASS
These can be configured in your config.yml with your own exceptions:
overblog_graphql:
definitions:
exceptions:
types:
errors: "AppBundleExceptionsUserErrorException"
To understand it at a deeper level, you should look at the class that implements this handling:
OverblogGraphQLBundleErrorErrorHandler"
The ErrorHandler is also responsible for putting the stack trace on the response. It will only do this if Symfony’s debugging is turned on. That is normally done on the “$kernel = new AppKernel(‘dev’, true);” call.
I encourage you to test sending a non-existent id to that query with debug=true and with debug=false and see the response. Exceptions are logged on dev.log.
Final Thoughts
Having used GraphQL for several months, I can add to the chorus of positive reviews. It’s a query language that shows several key benefits over REST.
- It enables us to think from an ‘API first’ perspective. Only involving the DB at the end of the process. By which point we are sure the application we have built works.
- Because the language has been battle tested in the trenches by Facebook, it will save you unnecessary headaches.
- It empowers the client developer, giving him more independence to add or remove fields and relations on the queries: speeding up the development time.
- Finally, because it has been built ‘by a business for business’, it enables you to create clean and logical code that will satisfy your clients needs more precisely.
I would love to hear if this GraphQL Tutorial helped you progress with the development of your own apps or if you have any questions about my process. You can read more our developement articles here.
Are you searching for your next programming challenge?
Scalable Path is always on the lookout for top-notch talent. Apply today and start working with great clients from around the world!