Quantcast
Channel: Featured – JohnQuarto.com
Viewing all articles
Browse latest Browse all 14

GraphQL on Rails, part 2/2

$
0
0

A exploration of using GraphQL to query a Rails 5 API

In a previous post, I described some of the minor frustration our team experienced using REST. This post is more tactical. What I’d like to do here is to set up the absolute minimum you need to see GraphQL in action on a throw-away Rails app.

There are a ton of examples of starting your first serious app with Rails and GraphQL — I watched an excellent YouTube video from a fellow who spent the first 15 min setting up the Rails app and then got around to adding in GraphQL. We don’t need another one of those; his was fine (watch it here).

So I’ll skip the bells and whistles and let’s get right to some code.

Our simple app is an API we can query: We have a cute little town called Catoville, and our domain model consists of:

  • some office Buildings; each has a name, and a year it was built.
  • some coffee houses, too, so people working in the Buildings can go get a caffeine fix; each of which also has a name and a telephone number
  • we magically “know” the walk time & distance of a Building to a Coffee House. For simplicity we’ll model it as a ∞:∞ relationship named Caffeinations, rather than getting all GIS on ourselves with longitude/latitude and great circles and the like.
  • and it’s a public API, so we won’t worry about an auth token.

That’s it, otherwise it wouldn’t be a simple example. I’ll assume you don’t need to see a step-by-step of setting up a standard Rails 5 api app and we can concentrate on GraphQL

We’ll start with Models along the lines of the following:

# Table name: buildings
#
# name :string
# year_built :integer

class Building < ApplicationRecord
has_many :caffeinations
has_many :coffee_houses, through: :caffeinations
end

# Table name: coffee_houses
#
# name :string
# telephone :string

class CoffeeHouse < ApplicationRecord
has_many :caffeinations
has_many :buildings, through: :caffeinations
end

# Table name: caffeinations
#
# building_id :integer
# coffee_house_id :integer
# walk_time :float
# walk_distance :float

class Caffeination < ApplicationRecord
belongs_to :building
belongs_to :coffee_house
end

I added in some data by hand based on the image above, since there were so few records. You might do it via a seed file or etc.

Our API will respond to the three following requests:

  1. Return a list of Buildings
  2. Given a Building id, return some details about it
  3. Given a Building id, return details about the Building’s associated Coffee Houses

Let’s get started:

Add the GraphQL gem to your Gemfile:

gem 'graphql'

You can add 

gem graphiql-rails
 since it lets you play around with the api, especially in a dev environment. But you probably already have something like POSTMAN in your browser, or similar, which is all we really need.

The graphql gem can set up some standard directories and basic code for you. Not required, but very useful. let’s do that.

rails g graphql:install

Modify your application.rb paths:

config.autoload_paths << Rails.root.join('app', 'graphql')
config.autoload_paths << Rails.root.join('app', 'graphql', 'types')

At the time of this writing, I had to manually add the paths above. Hopefully this will get fixed in the install process soon.

Add a route for the API endpoint:

We want to respond to

POST /graphql?query={[your_graphql_query_here]}
 so our
routes.rb
  file looks like (yeah! the install process already set this up for us):
# config/routes.rb
Rails.application.routes.draw do
  post 'graphql', to: 'graphql#execute'
end

Add a GraphQL controller:

We got a free totally standard GraphQL controller set up by the install process, too! It’s standard because the schema we’re about to define actually does a lot of the controller’s work.

# controllers/graphql_controller.rb
class GraphqlController < ApplicationController
  def execute
    variables = ensure_hash(params[:variables])
    query = params[:query]
    operation_name = params[:operationName]
    context = {
      # Query context goes here, for example:
      # current_user: current_user,
    }
    result = CatovilleCoffeeSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
    render json: result
  end

...
end

You can see near the end of the 

execute
 controller method, that we’ll call the execute method on our schema. Wait! We got a standardized one of those too for free from install

As you can read on the GraphQL website, basically we’ll need to define some object types  we’re willing to serve up (in our example, for Buildings and CoffeeHouses), and what data on them we are willing to make available for consumption via API. Then we connect them to a schema that makes them available for querying. Then we execute queries against that schema.

Add a schema:

# graphql/catoville_coffee_schema.rb
CatovilleCoffeeSchema = GraphQL::Schema.define do
  query QueryType
  # mutation MutationType
end

As mentioned earlier, we won’t bother we any mutations, so ignore that for now. What we need here is to define what our QueryType should do.

Define the query types:

# graphql/types/query_types.rb

QueryType = GraphQL::ObjectType.define do
  name "Query"
  # Add root-level fields here.
  # They will be entry points for queries on your schema.

  field :buildings do
    type types[BuildingType]
    description 'all buildings'
    resolve -> (obj, args, ctx) {Building.all}
  end

  field :building do
    type BuildingType
    description 'a particular building, given an id'
    argument :id, !types.ID
    resolve -> (obj, args, ctx) {Building.find(args[:id])}
  end

end

Looks very controller+route-ish, right? “if you send me a request for ‘buildings’, I’ll return a array of BuildingTypes, and it will include whatever the BuildingType specifies it wants to do when it has a collection from the Building model via

Building.all
 ”

And, “if you send me a request for ‘building’, I’ll need an ‘id’ argument, and I’ll use it to return whatever the BuildingType specifies it wants to do when it has a single Building model instance”

All strong typed and introspect-able.

Define what we want BuildingType to return:

# graphql/types/building_type.rb

BuildingType = GraphQL::ObjectType.define do
  name 'Building'
  description ''
  field :id, !types.ID
  field :name, !types.String
  field :year_built, !types.Int
  field :coffee_houses, -> { types[CoffeeHouseType] }, 'places to have coffee'
end

So whenever a BuildingType is needed the above defines what data will be allowed out. Note two things:

  1. things are strongly typed, so you really know what you’re getting back. There’s an entire library of standard types you’d expect (String, Int, etc) as well user-defined types which is what we’re specifying right here
  2. a consumer of the API will only get back the subset of these BuildingType fields that it specifies in its query. So although BuildingType can tell you about 
    id
     , 
    name
     ,
    year_built
     and 
    coffee_houses
     , it’s only going to send you what you ask for.

This highlights one of the rationales for GraphQL: that the backend devs would specify all the things that the BuildingType can return, and then the front end devs (or consumers of the API) will specify what they need. Time saved in not having to go bug the backend people to write a new or modified end point, and no need to send large chunks of data which are available yet not needed by the requestor.

Define what we want CoffeeHouseType to return:

We’ll make a Coffee House data type too, because the Building type says it has the ability to return an array of coffee houses, so when it returns one what makes up each of those Coffee House objects?

# graphql/types/coffee_house_type.rb

CoffeeHouseType = GraphQL::ObjectType.define do
  name 'CoffeeHouse'
  description ''
  field :id, !types.ID
  field :name, !types.String
  # field :telephone, !types.String
end

Note I commented out the

telephone
  field, so even though it’s part of the model, CoffeeHouseType will no way of passing that to any request. In a “real” app, I’d’ve skipped including it all together.
Ready to call the API:

That’s pretty much it. Start the server, and let’s call the API! Remember, I’m using the POSTMAN browser plugin, but you can do this any way you like.

Of the 3 target endpoints we want to make available, the API call #1 (“give me all buildings”) looks like:

localhost:3000/graphql?query={buildings {name}}

I find it easier to just focus on the GraphQL itself, since the path is constant, so I’ll refer to the above as:

{ buildings
  { 
    name
  }
}

where 

buildings
 comes from our query_types.rb file wherein we described what we’d respond to, and name is the field of BuildingType that we want back, nothing else (at this time)

We get back a JSON response of

{
  "data": {
    "buildings": [
      {"name": "Bldg A"},
      {"name": "Bldg B"},
      {"name": "Bldg C"},
      {"name": "Bldg D"},
      {"name": "Bldg E"},
      {"name": "Bldg F"}
    ]
  }
}

Precisely what we asked for, no more (we always get “data” as the top-level portion of the successful response).

Oh, you wanted the year the building was built? Well, just ask for it

{ buildings
  { 
    name
    year: year_built
  }
}

asking to give me back 

name
  and 
year_built
 but call it simply 
year
{
  "data": {
    "buildings": [
      {"name": "Bldg A", "year": 1950},
      {"name": "Bldg B", "year": 1975},
      {"name": "Bldg C", "year": 2000},
      {"name": "Bldg D", "year": 2016},
      {"name": "Bldg E", "year": 2017},
      {"name": "Bldg F", "year": 2018}
    ]
  }
}

How about API call #2? ‘Give me a particular building, with an id I specify’, here I want building with id=3, and the 

name
  and 
year_built
 :
{ building(id=3)
  { 
    name,
    year_built
  }
}

yielding:

{
  "data": {
    "building": {
      "name": "Bldg C",
      "year_built": 2000
    }
  }
}

And, finally, API call #3, info about building 5 and coffee houses associated with it. Oh, and let’s call this a 

office_building
 rather than a
building
 :
{ office_building: building(id=5)
  { 
    name,
    coffee_houses
      {
        name
      }
  }
}

which lets us easily return associated records, in a single call. Note, too, I do need to tell the CoffeeHouseType which of its fields I want back:

{
  "data": {
    "office_building": {
      "name": "Bldg E",
      "coffee_houses": [
        {"name": "Java Cat"},
        {"name": "House o' Joe"}
      ]
    }
  }
}

Whew! That’s it! Anything more and this would be an outright tutorial. But it does give a quick overview of how easily we can use GraphQL as part of an API.

Things I wish GraphQL would do (maybe it does, and I haven’t found it yet):

  • When I want ALL fields of a data type, I wish that I could get them without having to enumerate. Or, if I specify none, then perhaps it ought to mean all? This was a very simple example, but I can imagine a much more robust data type with a dozen fields. Or maybe this functionality exists, and I just missed it.
  • And, I feel a slight bother that we’ve delegated so much of the routes and controller functionality into the GraphQL schema. Yes, I can have many files in the
    app/graphql/types
     directory to break things up. But I feel some sort of abstraction might be possible to put a lot of the schema functionality back into the controller. Or maybe this is just better thought of Model-Presenter ?

Definitely I encourage you to seek out all the many additional GraphQL tutorials out there in the wild, now that you know how to put the pieces together with your Rails-based API. And GraphQL can happily co-exist with your “regular” REST API, so you can slowly expermient on existing projects!

 


Viewing all articles
Browse latest Browse all 14

Latest Images

Trending Articles





Latest Images