Sofware Development
Setting up your first Rails + GraphQL API
July 13, 2020
by
felipekb
import useBaseUrl from "@docusaurus/useBaseUrl";_In our previous [GraphQL article](./introduction-to-graphql), we went through the basics and some recommendations on how to use GraphQL. In this new article, we are going to explain how to build a basic GraphQL API using [Rails](https://rubyonrails.org/)._We will build a basic blog application. Our application will have multiple Users and they will be associated with multiple Posts.For the sake of simplicity, we will not include authentication. We managed to authenticate users with [devise-token_authenticable](https://github.com/baschtl/devise-token_authenticatable) by following this [tutorial](https://technology.doximity.com/articles/token-authentication-with-rails-vue-graphql-and-devise). You can check our project [repository](https://github.com/NeoCoast/rails-graphql-example/) and see how we implemented it.To follow this guide you'll need [Ruby](http://rubyonrails.org.es/instala.html), [RubyGems](http://rubyonrails.org.es/instala.html) and [PostgreSQL](https://www.postgresql.org/docs/9.3/tutorial-install.html).## Step 1: Initialize a rails project```rb$ rails new --api -T -d postgresql```## Step 2: Generate user and postCreate migrations for the model User and Post.```rbclass CreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| t.string :email, null: false, default: "" t.timestamps null: false t.string :first_name t.string :last_name end add_index :users, :email, unique: true endend```Our post model will only have a title and body.```rbclass CreatePosts < ActiveRecord::Migration[6.0] def change create_table :posts do |t| t.belongs_to :user, null: false, foreign_key: true t.string :title t.text :body t.timestamps end endend```Create the database and run the migrations.```sh$ rails db:create$ rails db:migrate```Add a `has_many` relation to the User model and a `belongs_to` to the Post model.```rb# app/model/user.rbclass User < ApplicationRecord # ... has_many :postsend# app/models/post.rbclass Post < ApplicationRecord # ... belongs_to :userend```## Step 3: Add GraphQL, [GraphiQL Gem](https://github.com/rmosolgo/graphql-ruby), and install gems.```rb# Gemfilegem 'graphql', '~> 1.10', '>= 1.10.9'gem 'graphiql-rails', '~> 1.7'```And then run `$ bundle install`## Step 4: Generate GraphQL files.```sh$ rails g graphql:install```## Step 5: Mount GraphiQL.If necessary, **create** `app/assets/config/manifest.js` and link the GraphiQL assets.```js//= link graphiql/rails/application.css//= link graphiql/rails/application.js```Given that we initialized our project as an API, we will need to **require [sprockets](https://github.com/rails/sprockets) on our application.rb** to be able to mount GraphiQL.```rb# config/application.rbrequire "sprockets/railtie"...```**Include GraphiQL route at routes.rb**```rbRails.application.routes.draw do post "/graphql", to: "graphql#execute" if Rails.env.development? mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql" endend```The GraphiQL route should only be included in the development environment. If we include GraphiQL in production, intruders can access our graph schema and possibly exploit vulnerabilities in our App.## Step 6: Define typesAs discussed in our [previous article](./introduction-to-graphql), our schema is defined by the types, queries, mutations, and subscriptions.Let’s start by defining our UserType.```rb# app/graphql/types/user_type.rbmodule Types class UserType < Types::BaseObject field :id, ID, null: false field :email, String, null: false field :first_name, String, null: false field :last_name, String, null: false field :full_name, String, null: false field :posts, [Types::PostType], null: true def full_name "#{object.first_name} #{object.last_name}" end endend```
Our UserType has every attribute that we defined for our user model except for the timestamps, you can include them by adding them as a field.
Note that the `full_name` field is calculated based on the current object.Then create the PostType.```rb# app/graphql/types/post_type.rbmodule Types class PostType < Types::BaseObject field :id, ID, null: false field :title, String, null: false field :body, String, null: false field :user, Types::UserType, null: false, preload: :user field :created_at, String, null: false endend```Note that we are using `preload` on our `user` field, this DSL (Domain-specific Language) is provided by the [graphql-preload gem](https://github.com/ConsultingMD/graphql-preload), when loading an object it preloads the specified associations, this helps prevent [N+1 queries](https://www.sitepoint.com/silver-bullet-n1-problem/).## Declare Queries and Mutations.Now that we have our types and models created and linked, let see how Queries and Mutations are built!Take a look at the autogenerated GraphQL files, among them you can find `_schema.rb`, `mutation_type.rb` and `query_type.rb`.We can define queries on the `query_type.rb`, but to keep the project structure as clean as possible, we are going to declare them at the _graphql/queries_ folder (if you don’t have it, just create it).First, we will have to define a BaseQuery.```rb# app/graphql/queries/base_query.rbmodule Queries class BaseQuery < GraphQL::Schema::Resolver endend```Now we are ready to create our first query!Notice that we are defining the return type with the _type_ keyword, we are using `PostType.connection_type` to benefit from pagination, more on that at the [graphql-ruby documentation](https://graphql-ruby.org/pagination/using_connections.html).```rb# app/graphql/queries/posts.rbmodule Queries class Posts < Queries::BaseQuery description 'list all posts' type Types::PostType.connection_type, null: false def resolve ::Post.all end endend```
You can define extra methods here, the resolver will be the one in charge of fulfilling the request.
In a similar manner, we define the `users` query.```rb# app/graphql/queries/users.rbmodule Queries class Users < Queries::BaseQuery description 'list all Users' type Types::UserType.connection_type, null: false def resolve ::User.all end endend```So far we’ve built queries to retrieve all the users and all the posts. Note that neither of these two queries had input values. If you wanted to have input fields on a query you can specify them as arguments.Let’s build queries using arguments, for example, retrieve users and posts by id.```rb# app/graphql/queries/user.rbmodule Queries class User < Queries::BaseQuery description 'get user by id' type Types::UserType, null: false argument :id, Integer, required: true def resolve(id:) ::User.find(id) end endend``````rb# app/graphql/queries/post.rbmodule Queries class Post < Queries::BaseQuery description 'get post by id' type Types::PostType, null: false argument :id, Integer, required: false def resolve(id:) ::Post.find(id) end endend```For the pagination to work we will have to enable the connection_type at the `_schema.rb` (this should be enabled by default).```rb# app/graphql/_schema.rbclass GraphqlApiSchema < GraphQL::Schema mutation(Types::MutationType) query(Types::QueryType) # Add built-in connections for pagination use GraphQL::Pagination::Connectionsend```Then, for us to add this query to the schema, we will have to specify a _field_ for this query on our query type with the resolver we just declared.If you are familiar with rails, then this step should look similar to defining routes for new endpoints, just specify the name of the _filed_ and the _resolver_.```rb# app/graphql/types/query_type.rbmodule Types class QueryType < Types::BaseObject field :posts, resolver: Queries::Posts field :users, resolver: Queries::Users field :user, resolver: Queries::User field :post, resolver: Queries::Post endend```### Typing out our queriesUp to this point, our database is empty. We can manually create Users and Posts from the rails console. Once we have some models in our database we can start testing our queries.Start the rails server, open a browser, and go to http://localhost:3000/graphiql.You can write and execute queries at GraphiQL. Also, thanks to the [introspections queries](https://graphql.org/learn/introspection/) it comes packed with auto-completion!
%7D)
In the right box, we can write down the query. Once executed the result will come up on the left-side section.
### MutationsFor mutations, we won’t need to generate a folder, because it’s created by default.Given that we are going to use very similar inputs to update and create posts, we strongly suggest defining a `PostAttribute` input-type to specify the common input parameters, you can specify the required arguments one by one for each mutation, but you will probably end up with repeated code blocks **(Don’t Repeat Yourself)**.```rb# app/graphql/types/post_attributes.rbmodule Types class PostAttributes < Types::BaseInputObject description "Attributes for creating or updating a post" argument :title, String, required: false argument :body, String, required: false endend```
The fields of the inputObjects are specified as arguments.
When defining a mutation try to use consistent naming. For example, we will create two mutations CreatePost and UpdatePost.```rb# app/graphql/mutations/create_post.rbmodule Mutations class CreatePost < Mutations::BaseMutation graphql_name "CreatePost" argument :attributes, Types::PostAttributes, required: true argument :user_id, Integer, required: true field :post, Types::PostType, null: false def resolve(attributes:, user_id:) user = User.find(user_id) post = user.posts.create(attributes.to_h) MutationResult.call( obj: { post: post }, success: post.persisted?, errors: post.errors ) rescue ActiveRecord::RecordInvalid => invalid GraphQL::ExecutionError.new( "Invalid Attributes for #{invalid.record.class.name}: " \ "#{invalid.record.errors.full_messages.join(', ')}" ) end endend```
The fields of the inputObjects are specified as arguments.
```rb# app/graphql/mutations/update_post.rbmodule Mutations class UpdatePost < Mutations::BaseMutation graphql_name "UpdatePost" argument :attributes, Types::PostAttributes, required: true argument :id, ID, required: true field :post, Types::PostType, null: false def resolve(id:, attributes:) post = Post.find(id) post.update(attributes.to_h) MutationResult.call( obj: { post: post }, success: post.persisted?, errors: post.errors ) rescue ActiveRecord::RecordInvalid => invalid GraphQL::ExecutionError.new( "Invalid Attributes for #{invalid.record.class.name}: " \ "#{invalid.record.errors.full_messages.join(', ')}" ) end endend```And then, we add it to the `mutation_type.rb` as a field like we did for queries.```rb# app/graphql/types/mutation_type.rbmodule Types class MutationType < Types::BaseObject field :create_post, mutation: Mutations::CreatePost field :update_post, mutation: Mutations::UpdatePost field :create_user, mutation: Mutations::CreateUser endend```## Testing your APIFor testing integration, we can call the `Schema.execute(query)`. This method will execute the given query on our schema and return the response object.For example, let us test the list all posts query.```rbrequire "rails_helper"RSpec.describe Queries::Posts do describe "posts" do let!(:users) { create_list(:user, rand(1..10)) } let!(:posts) { create_list(:post, rand(1..10), user: users.sample(1)[0]) } let(:query) do %(query { posts { nodes{ title body user{ firstName lastName } } } }) end subject(:result) do SimplifiedApiSchema.execute(query).as_json end it "should return the created post" do expect(result.dig("data", "posts", "nodes")).to match_array( posts.map do |post| { "title" => post.title, "body" => post.body, "user" => { "firstName" => post.user.first_name, "lastName" => post.user.last_name } } end ) end endend```
When using authentication, you can specify the current_user as context when calling the execute method.
```rbrequire "rails_helper"RSpec.describe Mutations::CreateUser do describe "Create user with proper data" do let(:user_attrs) { attributes_for(:user) } let(:mutation) do %(mutation{ createUser( input:{ attributes: { firstName: "#{user_attrs[:first_name]}", lastName: "#{user_attrs[:last_name]}", email: "#{user_attrs[:email]}" } } ) { user { email } } }) end subject(:result) do SimplifiedApiSchema.execute(mutation).as_json end it "should create user" do expect(result.dig("data", "createUser", "user")).to include({ "email" => user_attrs[:email] }) end endend```If we wanted to test transport-layer behavior, we could rewrite the spec to `POST` the query to our GraphQL endpoint (keep in mind that our GraphQL endpoint only receives `POST` requests).```rbrequire "rails_helper"RSpec.describe Mutations::CreateUser, type: :request do describe "POST /graphql create_user with proper data " do let(:user_attrs) { attributes_for(:user) } let(:mutation) do %(mutation{ createUser( input:{ attributes: { firstName: "#{user_attrs[:first_name]}", lastName: "#{user_attrs[:last_name]}", email: "#{user_attrs[:email]}" } } ) { user { email } } }) end let(:expected_response) do { "data" => { "createUser" => { "user" => { "email" => user_attrs[:email] } } } } end it "should return my email" do post '/graphql', params: { query: mutation } expect(JSON.parse(response.body)).to eq(expected_response) end endend```## Suggestions- **Use [graphql-preload](https://github.com/ConsultingMD/graphql-preload) gem:** it allows ActiveRecord associations to be preloaded in field definitions. Preloading nested associations will improve the performance overall, given that it prevents N+1 queries.- For token-based authentication, you can use `devise-token_authenticable` gem, this [tutorial](https://technology.doximity.com/articles/token-authentication-with-rails-vue-graphql-and-devise) covers authentication using that gem.- By default the GraphQL gem wants you to declare the queries at `query_type.rb`, this can make this file quite large. To avoid this kind of issues **create a Queries folder**, then glue them together at the `query_type.rb`, this will keep a cleaner code at the query_type.rb .- When using [GraphiQL](https://github.com/rmosolgo/graphiql-rails) gem, make sure that the route generated isn’t included in production.- At mutations, **define types for inputs**. This is a good practice because you can reuse this type for multiple mutations and you won’t have to rewrite the same inputs for similar mutations **(DRY)**.- **Use pagination**. The connection type comes with the [graphql-ruby](https://github.com/rmosolgo/graphql-ruby) gem. Check this [guide](https://graphql-ruby.org/pagination/using_connections.html) to see how to use the `connection_type`.