GraphQL Persisted Queries with HTTP Caching [Part 3]
Generated with Carbon.now.sh
This is the third part of a four-part series on GraphQL Pesisted Queries with HTTP Caching. As a recap of part two, we created an Express server and React application, both using persisted queries.
In part three we will cover the following topics:
Follow along with the complete code changes on GitHub
We’ll create a basic Rails server that uses graphql-ruby
. We will gloss over the busy work of setting up the models, database, and GraphQL types. First, we have our route defined that will accept POST
requests to /graphql
and passes them to our controller.
# config/routes.rb
Rails.application.routes.draw do
post "/graphql", to: "graphql#execute"
end
The controller then extracts the variables, query, and operation name from the request’s parameters. All this information is then executed against the GraphQL schema.
# app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
def execute
variables = ensure_hash(params[:variables])
query = params[:query]
operation_name = params[:operationName]
result = RailsGraphqlSchema.execute(query, variables: variables, operation_name: operation_name)
render json: result
end
private
def ensure_hash(ambiguous_param)
# ... Generated code provided by graphql-ruby's graphql:install
end
end
Follow along with the complete code changes on GitHub
To accommodate persisted queries, we will have to synchronize the queries from our React application to our Rails server. Fortunately, persistgraphql-signature-sync
(our script from earlier to extract queries) does this already.
It is possible to sync the persisted queries to a specified endpoint. The endpoint needs to accept a POST request with body parameters of query and signature.
We will need to do a few things to support this new synchronization endpoint.
Let’s create a new PersistedQuery
model and migration:
# app/models/persisted_query.rb
class PersistedQuery < ApplicationRecord
end
# db/migrate/20180617011135_create_persisted_queries.rb
class CreatePersistedQueries < ActiveRecord::Migration[5.2]
def change
create_table :persisted_queries do |t|
t.string :signature, index: { unique: true }
t.string :query
t.timestamps
end
end
end
We will store the persisted queries in this newly created table – notice that we have a unique index on the signature. Now let’s add the required route and controller that will accept the synchronization request.
# config/routes.rb
Rails.application.routes.draw do
# ... existing routes
post "/graphql_persist", to: "graphql_persist#execute"
end
# app/controllers/graphql_persist_controller.rb
class GraphqlPersistController < ApplicationController
def execute
document = GraphQL.parse(params[:query])
if valid_query?(document)
persisted_query = PersistedQuery.create(
signature: params[:signature],
query: params[:query],
)
render json: persisted_query.attributes
else
render json: { errors: @errors }, status: 500
end
rescue StandardError => e
render json: { errors: [e.message] }, status: 500
end
private
def valid_query?(document)
query = GraphQL::Query.new(RailsGraphqlSchema, document: document)
validator = GraphQL::StaticValidation::Validator.new(schema: RailsGraphqlSchema)
results = validator.validate(query)
errors = results[:errors] || []
@errors = errors.map(&:message)
@errors.empty?
end
end
In this controller, we are parsing out the query and validating it against our schema. If the query is okay from a schema perspective, then we create a new PersistedQuery
database record with the query
and the signature
. When we get to using the persisted queries, we can do a quick look up and pull the query to be used.
We can now run the persistgraphql-signature-sync
command:
node index.js --input-path=../react-graphql/src --sync-endpoint=http://localhost:3000/graphql_persist
This will attempt to synchronize each query to the server. It is not the prettiest, but the command will print out the server’s response for each query.
Synching persisted query a38e6d5349901b395334b5fd3b14e84a7ca7c4fc060a4089f2c23b5cf76f0f80
{ id: 1,
signature: 'a38e6d5349901b395334b5fd3b14e84a7ca7c4fc060a4089f2c23b5cf76f0f80',
query: 'query ConsolesByYear($afterYear: Int, $beforeYear: Int) {\n consoles(afterYear: $afterYear, beforeYear: $beforeYear) {\n ...ConsoleFieldsFragment\n company {\n name\n __typename\n }\n __typename\n }\n}\n\nfragment ConsoleFieldsFragment on Console {\n name\n releaseYear\n __typename\n}\n',
created_at: '2018-07-03T19:52:54.717Z',
updated_at: '2018-07-03T19:52:54.717Z' }
Follow along with the complete code changes on GitHub
The finish line is near! Our Rails server has the queries persisted in the database. Now we just have to adjust our GraphqlController
to pull the appropriate query when receiving the persisted query request from our React application.
# app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
def execute
variables = ensure_hash(params[:variables])
query = params[:query]
operation_name = params[:operationName]
if query.present?
result = RailsGraphqlSchema.execute(query, variables: variables, operation_name: operation_name)
else
signature = params.dig(:extensions, :persistedQuery, :sha256Hash)
persisted_query = PersistedQuery.find_by!(signature: signature)
result = RailsGraphqlSchema.execute(persisted_query.query, variables: variables, operation_name: operation_name)
end
render json: result
rescue StandardError => e
render json: { errors: [e.message] }
end
# ... rest of class
end
Our controller’s action now handles the situation when we don’t have a query present, which is the case when we’re using persisted queries. In this situation, we pull out the sha256Hash
value from the parameters and look up the persisted query. We then execute the persisted query’s query against the schema.
In the event that we want to lock down the API to only use persisted queries, we can use a conditional like Rails.env.production?
to gate the flow, allowing only the persisted queries through.
In this post, we created a Rails server that exposes a GraphQL API. We used persistgraphql-signature-sync
to assist in synchronizing the queries from the React application (that we built in part two) to our Rails server.
Note: there does exist a paid pro version of graphql-ruby
called GraphQL::Pro, which has its own support for persisted queries. It is a great solution as it covers synchronization of queries, admin dashboard, and connection to clients (Apollo/Relay). If you can afford the cost and want an off the shelf solution, it is something you could consider. For the purpose of this article, however, we will skip out on it.
In the next and last part of this series, we will look at the final goal of adding HTTP caching to our GraphQL API servers. With HTTP caching we can lessen the load on our servers and offer faster response times to the consumers of the API.
This topic was presented at GraphQL Toronto July 2018: