DEV Community

kuskoman
kuskoman

Posted on

[Tutorial] Creating simple blog app with Rails 6 and GraphQL Part #1

In this post I will try to show you how to create a simple blog app with GraphQL. GraphQL is one of the most popular topics this year, however I won't try to explain what is it. There are already many articles about it.
Also, don't treat this post as The Only Great Truth, I haven't even created "real" app using this technology yet, however I think that I can make a basic tutorial about usage of this technology.

It's my first post here, so hi everyone! Please leave your feedback, no matter how bad this tutorial is. Honestly I imagined it to be way better, but after spending few hours on it I don't want to just delete it (I though creating something like this is faster). Also, if you see mistakes in this tutorial, please correct me.

Introduction

In this tutorial I will use:

  • Rails 6.0.0.rc1
  • RSpec-Rails 4.0.0.beta2
  • Graphql gem for Rails, v. 1.9.7

There is a github repo with code from this guide aviable (https://github.com/kuskoman/Rails-GraphQL-tutorial)

Lets start from creating new Rails application in API-only mode, without test framework.

rails new graphql-blog --api -T

Now we need to add some gems to our Gemfile.
Lets modify our :development and :test group.

group :development, :test do
  [...]
  gem 'rspec-rails', '~> 4.0.0.beta2'
  gem 'factory_bot_rails', '~> 5.0', '>= 5.0.2'
  gem 'faker', '~> 1.9', '>= 1.9.4'
end

Then, lets create :test group below.

group :test do
  gem 'database_cleaner', '~> 1.7'
  gem 'shoulda-matchers', '~> 4.1'
end

Lets say a few words about our testing stack and why are we requiring test dependencies in :development: they aren't neccessary. However, if you don't require RSpec in dev dependencies you will need to execute RSpec-related commands from test enviroment. Also, spec files won't be automatically created by generators.
Faker with Factory Bot Rails are also very useful- they allow us to seed database from console really quickly (for example FactoryBot.create_list(:article, 30).

Lets install our gems.

bundle

Then, we can start to preparing our test enviroment. Lets start from generation RSpec install.

rails g rspec:install

then, we can require our Factory Bot methods. To do that we need to change spec/rails_helper.rb file by adding

RSpec.configure do |config|
  [...]
  config.include FactoryBot::Syntax::Methods
  [...]
end

Now, lets add database_cleaner config to same file.

RSpec.configure do |config|
  [...]
  config.include FactoryBot::Syntax::Methods
  [...]
  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
    DatabaseCleaner.strategy = :transaction
  end

  # start the transaction strategy as examples are run
  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end
  [...]

Then, we need to configure Shoulda-matchers. This config is also stored in same file, but outside RSpec.configure.

Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :rails
  end
end

Yeah! We finished setting up our test suite. Now we can move to... installing GraphQL.

Lets move back to our gemfile, and add graphql gem.

[...]
gem 'graphql', '~> 1.9', '>= 1.9.7'
[...]

Now we can generate its installation using rails generator.

rails g graphql:install

Because of --api flag during project creation we will probably see this information in console:

Skipped graphiql, as this rails project is API only
  You may wish to use GraphiQL.app for development: https://github.com/skevy/graphiql-app

GraphiQL is an app like Postman, but designed only for GraphQL and working in web browser. Of course we will need a tool instead web version of GraphiQL. We can just use its standalone version, or for example GraphQL Playground (which is unfortunately built on Electron, as well as have some annoying issues. But not annoying enough to force me to create issue on Github).
Recantly Postman also started supporting GraphQL.

Well, lets get back to the topic. As we can see, we created entire file structure in app/graphql folder. Let me say a few words about them.
mutations is a place for (yeah, that was unexpected) mutations. You can treat them a bit like post actions in REST api.
types is a place for types, as well as queries stored in query_type.rb. We can change this behavior, but we won't do it in this tutorial.

Unfortunately, before we start making GraphQL stuff, we need to create models, which are just same as in every other application.
Lets start from user model. This model will have secure password, so we need to add

gem 'bcrypt', '~> 3.1', '>= 3.1.13'

to our Gemfile (then obviously run bundle).

Because I (usually) love TDD I will start from generating user model, then immidiately moving to its specification.

rails g model user name:string password:digest

Now, lets create specification for our user model.
spec/models/user_spec.rb

require 'rails_helper'

RSpec.describe User, type: :model do
  let!(:user) { build(:user) }

  it 'has a valid factory' do
    expect(user).to be_valid
  end

  describe 'validation' do
    it { should validate_presence_of(:name) }
    it { should validate_presence_of(:password) }

    it { should validate_length_of(:name).is_at_least(2).is_at_most(32) }
    it { should validate_length_of(:password).is_at_least(6).is_at_most(72) }

    it { should validate_uniqueness_of(:name).case_insensitive }

    context 'username validation' do
      it 'should accept valid usernames' do
    valid_usernames = ['fqewqf', 'fFA-Ef231', 'Randy.Lahey', 'jrock_1337', '1234234235' ]

        valid_usernames.each do |username|
          user.name = username
            expect(user).to be_valid
          end
        end
      end

    it 'should not accept invalid usernames' do
      invalid_usernames = ['!@3', 'ff ff', '...', 'dd@dd.pl', 'wqre2123-23-', '-EW213123ed_d', '', '---', 'pozdro_.dla-_wykopu']

      invalid_usernames.each do |username|
        user.name = username

        expect(user).to be_invalid
      end
    end
  end
end

Now we can run our tests using rspec command and see failing 5 of 7 examples (yes, in real TDD it would be 7/7).

Let me add something about password validation- I wanted to allow user to use all alphanumeric characters and ., - and _, but not more than one time in a row and not in begining and end of ths nickname.

Lets fill our user model with code.

class User < ApplicationRecord
  has_secure_password

  validates :name, presence: true, length: { minimum: 3, maximum: 32 },
    format: { with: /\A[0-9a-zA-Z]+([-._]?[0-9a-zA-Z]+)*\Z/}, uniqueness: { case_sensitive: false }
  # actually pasword from has_secure_password has a 72 character limit anyway, but w/e
  validates :password, presence: true, length: { minimum: 6, maximum: 72 }
end

All tests should pass now. However, before we move to creating type lets change a bit user factory located in spec/factories/user.rb to add a bit of randomness.

FactoryBot.define do
  factory :user do
    name { Faker::Internet.username(separators = %w(._-)) }
    password { Faker::Internet.password(6, 72) }
  end
end

Now we can finally move to GraphQL stuff.

Lets start from creating user type. Lets (I think I'm using this word a little bit too frequently and its confusing) create user_type.rb in app/graphql/types directory.

module Types
    class UserType < Types::BaseObject
      description "Just user, lol"
      field :id, ID, null: false
      field :name, String, null: false
      field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    end
end

GraphQL Ruby syntax is pretty clear and I dont think that I need to explain anything in this code (change my mind).

Now lets make our first query.
Lets move to app/graphql/types/query_type.rb. We will see example file, generated by command rails g graphql:install.

module Types
  class QueryType < Types::BaseObject
    # Add root-level fields here.
    # They will be entry points for queries on your schema.

    # TODO: remove me
    field :test_field, String, null: false,
      description: "An example field added by the generator"
    def test_field
      "Hello World!"
    end
  end
end

We can test how it works if we want to. Lets launch rails server and connect to it using one of tools mentioned eariler. I will use GraphQL Playground, because I'm feeling most comfortable with this tool. After opening it will ask us about local file or endpoint. Lets choose endpoint and enter our application graphql endpoint adress (usually localhost:3000/graphql).

We should be able to access our current schema now.

By default GraphQL Playground will ask server about current schema every 2000 milliseconds, what can be really annoying if we have opened console. We can change this behaviour by editing config file.

We need to click Application -> Settings in the top bar of application, or just hit ctrl + comma.
Now we should be able to see line responsible for refreshing schema.

{
  "schema.polling.interval": 2000,
}

We can change it's value, but we need to remember that refreshing schema not enough often is also not a good idea.
Now lets get back to our GraphQL endpoint. Now we are able to make queries and mutations on automatically generated dummy fields. We can for example make a query:

query {
  testField
}

and recive json as response from server.

{
  "data": {
    "testField": "Hello World!"
  }
}

We should now make a test for our first query, but since it's very simple (yeah, it's not an excuse) I will skip it. If you want to make test first, you can jump to bottom of this part of guide and read about testing mutations. Query tests are very similiar.
Anyway, lets move again to app/graphql/types/query_type.rb and enter code like this:

module Types
  class QueryType < Types::BaseObject
    field :user, UserType, null: false do
      description "Find user by ID"
      argument :id, ID, required: true
    end

    def user(id:)
      User.find(id)
    end
  end
end

Now lets say something about it.
field :user will create a new field named user and automaticaly look for a method user.
null: false tells that this field can't be empty. It means querying for user which does not exist will raise error.
description ... will create something like documentation, which we are able to use from our tools.
argument :id, ID, required: true requires to specify user id to use this query.

Lets manually test how it works.

query {
  user(id: 1) {
    name
  }
}

The code above should raise error, because this value can't be null, and we haven't created first user yet.

{
  "error": {
    "error": {
      "message": "Couldn't find User with 'id'=1",
      "backtrace": [...],
      "data": {}
  }
}

Lets fix it by adding users to database.
Lets open Rails console
rails c
and produce some users
FactoryBot.create_list(:user, 10)

Now, if we query again, we should see requested user data in JSON format.

{
  "data": {
    "user": {
      "name": "keren"
    }
  }
}

We can create another query.
app/graphql/types/query_type.rb

module Types
  class QueryType < Types::BaseObject
    field :user, UserType, null: false do
      description "Find user by ID"
      argument :id, ID, required: true
    end

    field :all_users, [UserType], null: true do
      description "Find all users"
    end

    def user(id:)
      User.find(id)
    end

    def all_users
      User.all
    end
  end
end

Now we can move to creating first mutations.

Because of lack of tools for organizing GraphQL and/or only because my lack of knowledge about them, we are going to place entire mutation in test file as a string. Of course, if we want to, we can move it to another support file, but I don't like doing it that way.
Some time ago I found a very useful piece of code in this article.
We will start from copying it to spec/support/graphql/mutation_variables.rb file.

module GraphQL
    module MutationVariables
        def mutation_variables(factory, input = {})
            attributes = attributes_for(factory)

            input.reverse_merge!(attributes)

            camelize_hash_keys(input).to_json
        end

        def camelize_hash_keys(hash)
            raise unless hash.is_a?(Hash)

            hash.transform_keys { |key| key.to_s.camelize(:lower) }
        end
    end
end

What does it do? It takes :factory and optional attributes as argument. When we pass factory it is transforming it, allowing us to use them as mutation variables. If we also pass additional arguments, they will replace attributes from factory (here we can read about camelize_hash_keys method ).

After creating this module, we can include it to RSpec tests.

spec/rails_helper.rb

RSpec.configure do |config|
  [...]
  config.include GraphQL::MutationVariables
  [...]
end

Other helper I really like is RequestSpecHelper I saw in (this tutorial)[https://scotch.io/tutorials/build-a-restful-json-api-with-rails-5-part-one].

It looks like this:

module RequestSpecHelper
  def json
    JSON.parse(response.body)
  end
end

then we also need to add

RSpec.configure do |config|
  [...]
  config.include RequestSpecHelper
  [...]
end

in our rails_helper.rb.

For now we will create a not really useful mutation, just to show how it works.
First thing we have to do is creating new file in app/graphql/mutations. Lets name it create_user.rb. Before filling in this file lets create spec/graphql/mutations/create_user_spec.rb and move to this file.

require 'rails_helper'

RSpec.describe 'createUser mutation', type: :request do
    describe 'user creation' do
        before do
            post('/graphql', params: {
              query: %(
                    mutation CreateUser(
                        $name: String!,
                        $password: String!,
                    ) {
                        createUser(input: {
                            name:$name,
                            password:$password,
                        }) {
                            user {
                                id
                                name
                            }
                            errors,
                        }
                    }
                ),
              variables: mutation_variables(:user, input_variables)
            })
        end

        context 'when input is valid' do
            let(:user_attrs) { attributes_for(:user) }
            let(:input_variables) { user_attrs }

            it 'returns no errors' do
                errors = json["data"]["createUser"]["errors"]
                expect(errors).to eq([])
            end

            it 'returns username' do
                user_name = json["data"]["createUser"]["user"]["name"]
                expect(user_name).to eq(user_attrs[:name])
            end
        end

        context 'when input is invalid' do
            context 'when username is empty' do
                let(:input_variables) { {"name": ""} }

                it 'returns errors' do
                    errors = json["data"]["createUser"]["errors"]
                    expect(errors).not_to be_empty
                end

                it 'does not return user' do
                    user = json["data"]["createUser"]["user"]
                    expect(user).to be_nil
                end
            end

            context 'when password is invalid' do
                let(:input_variables) { {"password": "d"} }

                it 'returns errors' do
                    errors = json["data"]["createUser"]["errors"]
                    expect(errors).not_to be_empty
                end

                it 'does not return user' do
                    user = json["data"]["createUser"]["user"]
                    expect(user).to be_nil
                end
            end
        end
    end
end

After running rspec we should see result:
14 examples, 6 failures

Lets create base mutation file:
app/graphql/mutations/base_mutation.rb

module Mutations
    class BaseMutation < GraphQL::Schema::RelayClassicMutation
        object_class Types::BaseObject
        input_object_class Types::BaseInputObject
    end
end

Then lets fill our mutation.
app/graphql/mutations/create_user.rb

module Mutations
class CreateUser < BaseMutation
argument :name, String, required: true
argument :password, String, required: true

    field :user, Types::UserType, null: true
    field :errors, [String], null: false

    def resolve(name:, password:)
        user = User.new(name: name, password: password)
        if user.save
            {
                user: user,
                errors: [],
            }
        else
            {
                user: nil,
                errors: user.errors.full_messages,
            }
        end
    end
end

end

After running rspec again we will se just a same result. It's because we need to also add mutation to mutation_type.rb like we did with queries and query_type.
Default app/graphql/types/mutation_type.rb seems like this:

module Types
  class MutationType < Types::BaseObject
    # TODO: remove me
    field :test_field, String, null: false,
      description: "An example field added by the generator"
    def test_field
      "Hello World"
    end
  end
end

We need to replace it with:

module Types
  class MutationType < Types::BaseObject
    field :create_user, mutation: Mutations::CreateUser
  end
end

Lets get back to our mutation. What is happening here?
argument :name, String, required: true we are requiring name here, mutation without this argument will return error. Also we are specyfing type of input object (String).
argument :password, String, required: true same thing as above, just related to password.

field :user, Types::UserType, null: true
field :errors, [String], null: false
We are returning two objects- UserType, which can be null if user is not created, and errors, which is an empty array if user is created, or has user.errors.full_messages, if it isn't.

Now we can play with our mutation in our favourite GraphQL tool.

mutation {
  createUser(input:{
    name:"DDD",
    password:"leszkesmieszke"
  }) {
    user {
      id
      name
    }
  }
}
{
  "data": {
    "createUser": {
      "user": {
        "id": "15",
        "name": "DDD"
      }
    }
  }
}

Thats first part of tutorial. Honestly, I'm not sure if I'm going to create rest of them, it probably depends on reactions of this one. As I said, I imagined this tutorial as more than a bit better than it is.

Oldest comments (2)

Collapse
 
storrence88 profile image
Steven Torrence • Edited

Hi, I am trying to follow along with your tutorial. I am at the part where we have just added the mutations_variable.rb. I've included the line config.include GraphQL::MutationVariables to my rails_helper.rb inside of the Rspec.configure block.

The issue I am getting happens when I run rspec spec to initialize the tests.

Failure/Error: config.include GraphQL::MutationVariables
NameError: uninitialized constant GraphQL::MutationVariables

It doesn't seem to recognize the line I included in my rails_helper.rb. Any suggestions?

Collapse
 
kuskoman profile image
kuskoman

It's a typo, your file name must match module name, so GraphQL::MutationVariables
Must be in file mutation_variables.rb,
not mutations_variable.rb