graphql

A 2-post collection

Learnings from GraphQL Europe

This weekend, I had the pleasure to attend GraphQL-Europe, the first GraphQL conference in Europe.

Here is a recap of most notable things I learned there:

How GraphQL got from zero to wide adoption in one year

GraphQL spec was released less than an year ago. But it feels a lot more mature. This is because it was in production at Facebook for about 5 years before open-sourcing it.

The conference started with "GraphQL: Evolution or Revolution?" a great history lesson by Jonas Helfer.

In Five Years of Client GraphQL Infrastructure, Dan Schafer told us a couple of stories about how features like mutations were invented. There wasn’t much upfront design, they were just solving issue after issue. First in the frontend, then moving to the backend.

This connected very well with the Closing Keynote by Lee Byron. Where he talked about the future of GraphQL.

Those three talks convinced me that, GraphQL is not an accident.

Subscriptions

I haven't paid much attention of GraphQL Subscriptions in the past. they are now officially merged into the GraphQL spec.

In Realtime GraphQL from the Trenches, Taz Singh gave an interesting look into them.

I don't have many reasons to use them, mostly because Product Hunt doesn't need them at this time.

Persisted queries

Another feature I haven’t paid much attention to are persisted queries. Which is a miss on my part.

I had a couple conversations about persisted queries in between talks. Except for their obvious benefits - small requests, analytics and caching. I haven't realized they are also good from a security standpoint. Since if your GraphQL accepts only persisted queries in production, this makes impossible for somebody to open Chrome console and start firing expensive custom queries.

Handling errors

The Panel Discussion was very interesting. One of my major learnings from there - is that there isn't a standard way in GraphQL to handle form errors.

The format, I personally prefer looks something like this:

updateProfile(input: UpdateProfileInput) {  
  node: Profile
  errors: [Error]!
}

(I plan to write a blog post about form errors, someday)

I didn't know in GraphQL, you can return both a response and an error. It turns out that is a good strategy to handle failures in resolvers, which don't break the whole request. You can just return null for a given node and return errors explaining the failure.

{
  "errors": [
    {
      "message": "Service for fetching widget 1 failed",
      "locations": [ { "line": 4, "column": 5 } ],
      "path": [ "widget1" ]
    }
  ],
  "data": {
    "widget1": null,
    "widget2": {
      key: "value",
    }
  }
}

GraphQL in Ruby world

Ruby GraphQL gem, used by github and Shopify, is quite powerful and battle tested.

Netto Farah show a great solution for battling N+1 queries in Ruby project - GraphQL::QueryResolver. Also in the same talk, he gave great tips about better tracking of GraphQL. If you are using GraphQL with Ruby definitely check his slides.

Launching GitHub's Public GraphQL API gave great tips about:

  • handling authorization
  • using GraphQL for backfilling legacy REST endpoints
  • tips about schema design

Conclusion

Overall GraphQL-Europe was great. Kudos to Honeypot and Graphcool for organizing such great event.

p.s. I almost forgot! I finally found out what OData is :P.

Introducing SearchObject GraphQL Plugin

When I started using GraphQL, I immediately saw, that SearchObject would be a perfect fit for search resolvers.

Having a GraphQL query to fetch the first 10 most recent published news posts would look something like this:

query {  
  posts(first: 10, categoryName: "News", order: "RECENT", published: true) {    
    id  
    title  
    body  
    author {      
       id
       name
    }
  }
}

And it would have a corresponding SearchObject:

class Resolvers::PostSearch  
  include SearchObject.module  

  scope { Post.all }    

  option :categoryName, with: :apply_category_name_filter
  option :published, with: :apply_published_filter
  option :order, enum: %i(RECENT VIEWS LIKES)  

  # ... definitions of the option methods
end  

So clean. ☀️

But then, PostSearch have to be connected with GraphQL Ruby gem:

PostOrderEnum = GraphQL::EnumType.define do  
  name 'PostOrder'

  value 'RECENT'
  value 'VIEWS'
  value 'LIKES'
end  

Types::QueryType = GraphQL::ObjectType.define do  
  name 'Query'

  field :posts, types[Types::PostType] do
    argument :categoryName, types.String  
    argument :published, types.Boolean  
    argument :order, PostOrderEnum
    resolve ->(_obj, args, _ctx) { Resolvers::PostSearch.results(filters: args.to_h) } 
  end
end  

That isn't so bad. 🤔

But then, thinking about how can this code can change in the future:

  • adding/removing options would involve going to both files
  • adding a new order option would mean searching for the PostOrderEnum and manually sync it with PostSearch enum
  • reusing PostSearch in other types, for queries like: query { user(id: 1) { posts(published: true) } }
    • requires copy and paste argument/type definitions
    • which makes updating the resolver even harder

Yikes! 😤 😷

This is where SearchObject::Plugin::GraphQL comes in. It puts type definitions and the resolver itself:

class Resolvers::PostSearch  
  # include the the plugin
  include SearchObject.module(:graphql)

  # documentation and type about this resolver
  # can be provided into the resolver itself
  type types[Types::PostType]
  description 'Lists posts'

  # enums or other types can also be nested
  OrderEnum = GraphQL::EnumType.define do
    name 'PostOrder'

    value 'RECENT'
    value 'VIEWS'
    value 'LIKES'
  end

  scope { Post.all }    

  # options just need to have a their type specifed.
  option :categoryName, type: types.String, with: :apply_category_name_filter
  option :published, type: types.Boolean, with: :apply_published_filter  
  # enums are automatically handled
  option :order, type: OrderEnum

  # ... definitions of the option methods
end  

Then PostSearch can be used just as GraphQL::Function:

Types::QueryType = GraphQL::ObjectType.define do  
  name 'Query'

  field :posts, function: Resolvers::PostSearch
end  

Now, changing filter options requires changing only a single file. PostSearch can be reused in other types, by just adding function: Resolvers::PostSearch.

For more information check SearchObject::Plugin::GraphQL example.