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.
Top comments (2)
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 lineconfig.include GraphQL::MutationVariables
to myrails_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?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