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:
- Return a list of Buildings
- Given a Building id, return some details about it
- 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:
- 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
- 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!
One thought on “GraphQL on Rails, part 2/2”