In the past few weeks I’ve convinced myself that GraphQL can work well for any micro-service. This post is a full walk-through of getting a working GraphQL API on Ruby on Rails. This should help you get started, especially if you’ve never written a line of GraphQL in your life. The code for this post is dblock/graphql-invoices.
Prerequisites
Make a bare Rails API app with rails new --api
, get RSpec and RuboCop. See @c26b0e18.
GraphQL
Add gem 'graphql'
to the project.
This is the graphql-ruby library, an implementation of GraphQL in Ruby and is what is used for building a GraphQL endpoint on the server. See @7649ba2b.
Defining Schema
This project will implement a dummy invoice API where clients can get invoices and create invoices.
The invoice type has an ID and some fees in cents and goes into app/graphql/types/invoice.rb.
InvoiceType = GraphQL::ObjectType.define do name 'Invoice' description 'An Invoice' field :id, !types.Int field :fee_in_cents, types.Int end
The GraphQL ID type is a string, so we use Int
.
GraphQL defines a schema with queries (eg. get invoices) and mutations (eg. create an invoice), which typically goes into app/graphql/schema.rb.
Schema = GraphQL::Schema.define do query Query mutation Mutation end
The query root returns an invoice by ID, implemented in app/graphql/queries.rb. Notice that this takes an Int
(ID) and returns our InvoiceType
.
Query = GraphQL::ObjectType.define do name 'Query' field :invoice, InvoiceType do argument :id, !types.Int description 'Get an invoice by ID.' resolve ->(_obj, args, _ctx) { OpenStruct.new( id: args[:id], fee_in_cents: 20_000 ) } end end
A mutation creates invoices in app/graphql/mutations/create_invoice_mutation.rb. Use Relay, a data fetching framework, that makes it easy.
CreateInvoiceMutation = GraphQL::Relay::Mutation.define do name 'createInvoice' input_field :fee_in_cents, !types.Int return_type InvoiceType resolve ->(_object, inputs, _ctx) { OpenStruct.new( id: 1231, fee_in_cents: inputs[:fee_in_cents] ) } end
This mutation was referenced from the root schema above and is linked from app/graphql/mutations.rb.
Mutation = GraphQL::ObjectType.define do name 'Mutation' field :createInvoice, field: CreateInvoiceMutation.field end
GraphQL Controller
GraphQL accepts a single JSON payload via POST
in a typical Rails controller in app/controllers/graphql_controller.rb.
class GraphqlController < ApplicationController def execute result = Schema.execute( query, variables: variables, context: context, operation_name: operation_name ) render json: result end private def query params[:query] end def operation_name params[:operationName] end def context {} end def variables params[:variables] || {} end end
The controlled needs to be routed to in config/routes.rb.
Rails.application.routes.draw do post '/graphql', to: 'graphql#execute' end
GraphQL IDEs
The best way to try our app out is to use a GraphQL IDE. Run the Rails application with rails s
and point the IDE to https://localhost:3000/graphql
.
You can use various clients to consume the API from our applications, including graphlient or graphql-client.
Tests
Add graphlient, which is a small library built on top of graphql-client and that’s a bit easier to use.
Define a shared client context in spec/support/graphql/client.rb.
require 'graphlient' RSpec.shared_context "GraphQL Client", shared_context: :metadata do let(:client) do Graphlient::Client.new('https://api.example.org/graphql') do |client| client.http do |h| h.connection do |c| c.use Faraday::Adapter::Rack, app end end end end end
The client can fetch the schema in spec/graphql/schema_spec.rb.
require 'rails_helper' describe 'GraphQL Schema', type: 'request' do include_context 'GraphQL Client' it 'retrieves schema' do expect(client.schema).to be_a GraphQL::Schema end end
Fetch an invoice in spec/graphql/queries/invoice_query_spec.rb.
require 'rails_helper' describe 'Invoice Query', type: :request do include_context 'GraphQL Client' let(:query) do <<-GRAPHQL query($id: Int!) { invoice(id: $id) { id fee_in_cents } } GRAPHQL end it 'returns an invoice' do response = client.execute(query, id: 42) invoice = response.data.invoice expect(invoice.id).to eq 42 expect(invoice.fee_in_cents).to eq 20_000 end end
Create an invoice in spec/graphql/mutations/create_invoice_mutation_spec.rb.
require 'rails_helper' describe 'Create Invoice Mutation', type: :request do include_context 'GraphQL Client' let(:query) do <<-GRAPHQL mutation($input: createInvoiceInput!) { createInvoice(input: $input) { id fee_in_cents } } GRAPHQL end it 'returns an invoice' do response = client.execute(query, input: { fee_in_cents: 42_000 }) invoice = response.data.create_invoice expect(invoice.id).to eq 1231 expect(invoice.fee_in_cents).to eq 42_000 end end
Tooling
GraphQL comes with some impressive tooling and IDE integration, such as with Visual Studio Code via graphql-for-vscode.
Add a Rake task to dump the project’s schema to lib/tasks/graphql/schema.rake.
namespace :graphql do namespace :schema do directory 'data' desc 'Dump GraphQL API schema to data/schema.graphql.' task dump: [:environment, 'data'] do File.open('data/schema.graphql', 'w') do |f| f.write(Schema.to_definition) puts "Dumped schema to #{f.path}." end end end end
Add a configuration file, .gqlconfig
{ "schema": { "files": "data/schema.graphql" }, "query": { "files": [ { "match": "spec/graphql/**/*.rb", "parser": ["EmbeddedQueryParser", { "startTag": "<<-GRAPHQL", "endTag": "GRAPHQL" }] } ] } }
Install prerequisites for graphql-for-vscode.
brew update brew install watchman npm install @playlyfe/gql
Install graphql-for-vscode from the Visual Studio Code Marketplace, here.
Generating and Handling Errors
Read Generating and Handling Errors in GraphQL in Ruby next.