We Switched To GraphQL. This Is What We’ve Learned

Boris Bobrov, PHP Symfony Developer
Published: October 27, 2022Updated: October 31, 2022
6 min to read
We Switched To GraphQL. This Is What We’ve Learned

Introduction

There are many data exchange specifications: XML, JSON, GraphQL, and so on.

At Aristek, we’ve been using and evolving the JSON:API specification for many years. But now we’re gradually moving to GraphQL. In this article, we’ll discuss our GraphQL experience, the difficulties we’ve met, and the instruments we’ve used.

The Background

JSON:API was developed by Yehuda Katz in May 2013. The idea was to eliminate the need for ad-hoc code per application to communicate with servers that communicate in a well-defined way.

{
 "links": {
   "self": "http://example.com/articles",
   "next": "http://example.com/articles?page[offset]=2",
   "last": "http://example.com/articles?page[offset]=10"
 },
 "data": [{
   "type": "articles",
   "id": "1",
   "attributes": {
     "title": "JSON:API paints my bikeshed!"
   },
   "relationships": {
     "author": {
       "links": {
         "self": "http://example.com/articles/1/relationships/author",
         "related": "http://example.com/articles/1/author"
       },
       "data": { "type": "people", "id": "9" }
     },
     "comments": {
       "links": {
         "self": "http://example.com/articles/1/relationships/comments",
         "related": "http://example.com/articles/1/comments"
       },
       "data": [
         { "type": "comments", "id": "5" },
         { "type": "comments", "id": "12" }
       ]
     }
   },
   "links": {
     "self": "http://example.com/articles/1"
   }
 }],
 "included": [{
   "type": "people",
   "id": "9",
   "attributes": {
     "firstName": "Dan",
     "lastName": "Gebhardt",
     "twitter": "dgeb"
   },
   "links": {
     "self": "http://example.com/people/9"
   }
 }, {
   "type": "comments",
   "id": "5",
   "attributes": {
     "body": "First!"
   },
   "relationships": {
     "author": {
       "data": { "type": "people", "id": "2" }
     }
   },
   "links": {
     "self": "http://example.com/comments/5"
   }
 }, {
   "type": "comments",
   "id": "12",
   "attributes": {
     "body": "I like XML better"
   },
   "relationships": {
     "author": {
       "data": { "type": "people", "id": "9" }
     }
   },
   "links": {
     "self": "http://example.com/comments/12"
   }
 }]
}
Our team started using this specification to unify front-end coding. With JSON:API they could reuse the modules written by other departments.

Why GraphQL?

GraphQL was created in 2012 by Facebook (now Meta) for their mobile apps. Later, in 2015 GraphQL was open-sourced.

We started using GraphQL last year. The first thing we’ve noticed was the way GraphQL used the features of the HTTP protocol. It only used one URL address (typically “/graphql”) and only one HTTP method (typically POST). Meanwhile, JSON:API required using several HTTP methods and creating multiple addresses to access our recourses.

GraphQL’s flexibility gives us more options. For example, now we can create more flexible and convenient error reporting logic. Instead of using the limited list of pre-written HTTP replies, we can tailor the error system to our client’s business needs.

Here’s another difference. With GraphQL we can set up the response structure we want. But we can also adjust the query and data changing syntax.

In contrast to that, JSON:API has a rigid data structure. Unlike GraphQL, JSON:API has a set of required and optional fields. The query parameters are also prewritten.

{
    "data": {
        "articles": [
            {
	         "id": 1,
	         "title": "Article title",
	         "comments": [
		      {
		          "id": 1,
			   "text": "Good article!!!"
		      }
                ],
                "people": [
		      {
		          "id": 1,
			   "name": "Boris"
		      }
                ]
            }
        ]
    }
}

Another feature of GraphQL is that it supports type inheritance and multiple-type inheritance. You can use it instead of the 404 error from JSON:API. If the client app requests a Course resource that doesn’t exist, the server will return CourseNotFound. It will be the child object of Course and NotFound types. The client app can expect that the NotFound type can return, so it can request only the fields needed to display the error message.

These and many other differences lead us to use GraphQL for its simplicity and flexibility.

How Did We Begin With GraphQL?

For starters, we needed a library. We looked for one that would:

  • Support GraphQL
  • Have a large community
  • Be highly customizable
  • Be compatible with Symfony

After researching the libraries, we found out that the lowest level and the most popular one was “webonyx/graphql-php”. It’s perfectly customizable and compatible with most frameworks. The problem was that its basic functionality was very small.

This lead us to “api-platform/core”. Like “webonyx/graphql-php”, it was also customizable and had a good community that constantly improved the framework. Finally, it was compatible with Symfony.

API Platform

It takes a minute to install API Platform with GraphQL support. As the result, we got a fully functioning server that supported GraphQL.

API Platform supports many GraphQL features, but not all of them. Let’s take a look at the features that it supports out of the box.

Schemas & Types

GraphQL schema keeps the description of available queries, mutations, types, etc. Out of the box, API Platform automatically builds the schema based on the entities that are tagged with the attribute “#[Api\ApiResource]”. These can be:

  • Public fields
  • Get and set methods
  • Special attributes
#[Api\ApiResource]
class User
{
   public string $name;
  
   private string $email;

   public function getEmail(): string
   {
       return $this->email;
   }

   public function setEmail(string $email): self
   {
       $this->email = $email;

       return $this;
   }
}
type user implements Node {
 email: String!
 name: String!
}

This is where we met the current GraphQL limitations. Right now, API Platform can’t automatically create or work with Union and Interfaces types.

API Platform compares all the queries that have the schema. This lets us quickly find the ones with incorrect structure or unsupported data types.

We realized that we could use the schema for code testing. We wrote a command that would generate a human-readable schema and save it in the project. This way we could see how the schema reacted to the changes made by the developer. Even at the coding stage, we would see if the changes worked as planned. Also, at code review, the reviewer can right away see how the schema evolves. Now we have fewer errors when working with new instruments.

API Code

Queries & Mutations

In GraphQL, there’re only two ways to engage with the server. That is via queries and mutations. API Platform generates queries automatically for collections and individual items. It also generates mutations for CRUD operations. For example, this is what was automatically generated for our Course entity:

Query.courses
Query.course
Mutation.createCourse
Mutation.updateCourse
Mutation.deleteCourse
We can also use nested mutations. They can create several resources that would connect to each other.
mutation {
  createCourse(input: {
    name: "PHP for beginners",
    contentUnits: [{
      type: quiz
      free: true
    }]
  }) {
    course {
      id
      title
      contentUnits {
        collection {
          id
          free
          type
        }
      }
    }
  }
}

Customization Options

The first thing you run into when trying to customize queries and mutations is the Resolver. There are several stages to processing input data. Each stage can be customized. Below we have a diagram that describes the stages for each resolver type.

Resolver Structure

Resolver Structure

To make a custom query or mutation, you need to create a class that would implement one of the resolver interfaces. This will help to send logs when the resolver is called, or to change field inputs, etc.

Each stage is presented by a single service. This helps change the work so that it suits the customer’s needs.

Because we already had some modules written for JSON:API, we created mutations to reuse them. This way we could reuse our Security module that handled registration, login, user management, and ACL security.

One of the first issues we had was that we couldn’t rely on the HTTP response status anymore. Meanwhile, our current error system wasn’t unified enough.

To fix it, we developed our own error handlers that would change the structure and looks of the error messages. This way, it was much easier for the frontend team to differentiate between error types. It also helped figure out to which resource or action the errors belong. Our ErrorHandler would transform all errors by changing the error messages, codes, and traces.

final class ErrorHandler implements ErrorHandlerInterface
{
   public function __construct(
       private readonly ErrorHandlerInterface $defaultErrorHandler,
       private readonly ValidationErrorFactory $validationErrorFactory,
   ) {
   }

   public function __invoke(array $errors, callable $formatter): array
   {
       $extractedErrors = array_reduce(
           $errors,
           fn(array $carry, Error $error) => [...$carry, ...$this->getExtractedErrors($error)],
           [],
       );

       return ($this->defaultErrorHandler)($extractedErrors, $formatter);
   }

   /**
    * @return Error[]
    */
   private function getExtractedErrors(Error $error): array
   {
       $previousException = $error->getPrevious();

       if (!$previousException instanceof ValidationException) {
           return [$error];
       }

       return array_map(
           fn(ConstraintViolationInterface $violation) => $this->validationErrorFactory->createByErrorAndViolation(
               $error,
               $violation,
           ),
           iterator_to_array($previousException->getConstraintViolationList()),
       );
   }
}
In the end, our error messages got standard looks and data.

Conclusion

GraphQL is great for flexibility. It is still new to us, but we’ll continue using it on our PHP projects. We’ll go on exploring API Platform to build the products that serve our clients.

I hope, our case can help you switch to GraphQL. Take care!

Share:
Be the first to receive our articles

Relevant Articles


We use cookies to ensure that we give you the best experience on our website.
We also use cookies to ensure we show relevant content.