質問
A step-by-step guide to initializing your project to create a GraphQL API in Ruby on Rails API mode.
Update: 

Here are the steps for building a front-end environment based on Next.js.
We have chosen popular libraries as of September 2020 with the goal of being able to prototype efficiently.
We will use the following as our primary libraries

  • Framework: Ruby on Rails
  • GraphQL Lib: graphql-ruby, graphql-batch
  • Test Framework: RSpec
  • Fixure Management: Factorybot
  • Linter: Rubocop
  • Env Variable Management: dotenv
  • Model Annotation: annotate

1. Introduce Ruby on Rails Project

Create a new project:

rails new --api --database=postgresql PROJECT_NAME

1-2. Set default Ruby version

Set default Ruby version as follows:

echo '2.7.1' > .ruby-version

1-3. Add useful libraries

Add useful RubyGem libraries as follows:

# Configuration using ENV
gem 'dotenv-rails'

# GraphQL
gem 'graphql'
gem 'graphql-batch'

# Annotate schema and routes info
gem 'annotate'

# Use Puma as the app server
gem 'puma'
gem 'puma_worker_killer'

group :development, :test do
  # help to kill N+1
  gem 'bullet'

  # Pry & extensions
  gem 'pry-byebug'
  gem 'pry-rails'

  # Show SQL result in Pry console
  gem 'awesome_print'
  gem 'hirb'
end

group :development do
  # A Ruby static code analyzer
  gem 'rubocop', require: false

  gem 'rails-flog', require: 'flog'
end

group :test do
  # Mock for HTTP requests
  gem 'vcr'
  gem 'webmock'

  # test fixture
  gem 'factory_bot_rails'

  # Time Mock
  gem 'timecop'

  # Cleaning test data
  gem 'database_rewinder'

  # Rspec
  gem 'rspec-rails'

  # This gem brings back assigns to your controller tests
  gem 'rails-controller-testing'
end

After that, install thier RubyGem:

bundle install --path vendor/bundle --jobs=4 --without production

1-4. Setup basic configuration of Ruby on Rails

Setup basic configuration of Ruby on Rails with config/application.rb:

module AppName
  class Application < Rails::Application
    # Set timezone
    config.time_zone = 'Tokyo'
    config.active_record.default_timezone = :local

    # Set locale
    I18n.enforce_available_locales = true
    config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '', '*.{rb,yml}').to_s]
    config.i18n.default_locale = :ja

    # Set generator
    config.generators do |g|

      g.orm :active_record
      g.template_engine false
      g.test_framework :rspec, fixture: true
      g.fixture_replacement :factory_bot, dir: 'spec/factories'
      g.view_specs false
      g.controller_specs false
      g.routing_specs false
      g.helper_specs false
      g.request_specs false
      g.assets false
      g.helper false
    end
  end
end

1-7. Add Bullet Configuration

Add Bullet Configuration to config/environments/development.rb as follow:

Rails.application.configure do
  # Bullet Setting (help to kill N + 1 query)
  config.after_initialize do
    Bullet.enable = true # enable Bullet gem, otherwise do nothing
    Bullet.alert = true # pop up a JavaScript alert in the browser
    Bullet.console = true #  log warnings to your browser's console.log
    Bullet.rails_logger = true #  add warnings directly to the Rails log
  end
end

1-8. Modify DB Configuration

Modify Database Configuration config/database.yml as follows:

default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 50 } %>

development:
  <<: *default
  database: kikushiru_development
  host: 'localhost'
  port: <%= ENV['POSTGRES_PORT'] || '5432' %>

  username: <%= ENV['POSTGRES_USER'] %>
  password: <%= ENV['POSTGRES_PASSWORD'] %>

test:
  <<: *default
  database: kikushiru_test
  host: <%= ENV['POSTGRES_HOST'] || 'localhost' %>

  username: <%= ENV['POSTGRES_USER'] %>
  password: <%= ENV['POSTGRES_PASSWORD'] %>

production:
  <<: *default
  database: kikushiru_production
  username: kikushiru
  password: <%= ENV['POSTGRES_PASSWORD'] %>

Create database.

bundle exec rake db:create

1-9. Modify Puma Configuration

Modify Puma Configuration config/puma.rb to optimize local development as follows:

# Workers are forked web-server processes
workers ENV.fetch('RAILS_WORKERS') { Rails.env.development? ? 0 : 2 }.to_i

# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers a minimum and maximum.
threads_count = ENV.fetch('RAILS_MAX_THREADS') { Rails.env.development? ? 1 : 5 }.to_i
threads threads_count, threads_count

# Specifies the `port` that Puma will listen on to receive requests, default is 3000.
port ENV.fetch('PORT', 3000)

# Specifies the `environment` that Puma will run in.
environment ENV.fetch('RAILS_ENV') { 'development' }

# Use the `preload_app!` method when specifying a `workers` number.
preload_app!

before_fork do
  ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
end

on_worker_boot do
  # Worker specific setup for Rails 4.1+
  # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot
  ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end

# Allow puma to be restarted by `rails restart` command.
plugin :tmp_restart

1-10. Add a locale file

Add a local file config/locales for your user language as follows:

wget https://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/ja.yml -P config/locales/ja.yml

1-11. Create dot-env file

Create a dot-env file for development as follows:

touch .env
touch .env.sample
Write a comment

2. Add some configuration for useful libraries

2-1. Prepare initial configuration for annotate

Prepare initial configuration of annotate as follows:

bundle exec rails g annotate:install

2-2. Add RSpec confugration

Prepare initial configuration of RSpec command as follows:

bundle exec rails g rspec:install

echo '--require spec_helper --color -f d' > .rspec

Add RSpec configuration as follows:

require 'factory_bot_rails'

RSpec.configure do |config|

  config.order = 'random'

  config.before :suite do
    DatabaseRewinder.clean_all
  end

  config.after :each do
    DatabaseRewinder.clean
  end

  config.before :all do
    FactoryBot.reload
    FactoryBot.factories.clear
    FactoryBot.sequences.clear
    FactoryBot.find_definitions
  end

  config.include FactoryBot::Syntax::Methods

  VCR.configure do |c|

    c.cassette_library_dir = 'spec/vcr'
    c.hook_into :webmock
    c.allow_http_connections_when_no_cassette = true
  end

  %i[controller view request].each do |type|

    config.include ::Rails::Controller::Testing::TestProcess, type: type
    config.include ::Rails::Controller::Testing::TemplateAssertions, type: type
    config.include ::Rails::Controller::Testing::Integration, type: type
  end

  config.define_derived_metadata do |meta|

    meta[:aggregate_failures] = true unless meta.key?(:aggregate_failures)
  end
end

2-3. Modify Rubocop configuration

Modify code by Rubocop and generate configuration as following commands:

bundle exec rubocop --auto-correct
bundle exec rubocop --auto-gen-config

2-4. Initial graphql setup

Setup initial graphql configuration as following commands:

rails g graphql:install

Create app/graphql/foreign_key_loader.rb for graphql-batch:

class ForeignKeyLoader < GraphQL::Batch::Loader
  attr_reader :model, :foreign_key, :scopes

  def self.loader_key_for(*group_args)
    # avoiding including the `scopes` lambda in loader key
    # each lambda is unique which defeats the purpose of
    # grouping queries together
    [self].concat(group_args.slice(0, 2))
  end

  def initialize(model, foreign_key, scopes: nil)
    super()
    @model = model
    @foreign_key = foreign_key
    @scopes = scopes
  end

  def perform(foreign_ids)
    # find all the records
    filtered = model.where(foreign_key => foreign_ids)
    filtered = filtered.merge(scopes) if scopes.present?
    records = filtered.to_a

    foreign_ids.each do |foreign_id|

      # find the records required to fulfill each promise
      matching_records = records.select do |r|

        foreign_id == r.send(foreign_key)
      end
      fulfill(foreign_id, matching_records)
    end
  end
end

Create app/graphql/record_loader.rb for graphql-batch:

class RecordLoader < GraphQL::Batch::Loader
  def initialize(model, column: model.primary_key, where: nil)
    super()
    @model = model
    @column = column.to_s
    @column_type = model.type_for_attribute(@column)
    @where = where
  end

  def load(key)
    super(@column_type.cast(key))
  end

  def perform(keys)
    query(keys).each { |record| fulfill(record.public_send(@column), record) }
    keys.each { |key| fulfill(key, nil) unless fulfilled?(key) }

  end

  private

  def query(keys)
    scope = @model
    scope = scope.where(@where) if @where
    scope.where(@column => keys)
  end
end

Modify GraphQL schema information of app/graphql/app_name_schema.rb to use graphql-batch as follows:

class AppNameSchema < GraphQL::Schema
  mutation(Types::MutationType)
  query(Types::QueryType)

  # Opt in to the new runtime (default in future graphql-ruby versions)
  use GraphQL::Execution::Interpreter
  use GraphQL::Analysis::AST
  use GraphQL::Batch

  # Add built-in connections for pagination
  use GraphQL::Pagination::Connections
end

2-5. Sample GraphQL Resource

rails g model article title:string body:text

Create a sample GraphQL resolver with app/graphql/resolvers/article.rb as follows:

module Resolvers
  class Article < Base
    description 'Fetch article'
    type Types::ArtcileType, null: false

    argument :id, Int, required: true, description: 'ID'

    def resolve(id:)
      RecordLoader.new(::Article).load(id)
    end
  end
end

Create a sample GraphQL type with app/graphql/types/article_type.rb as follows:

module Types
  class ArticleType < Types::BaseObject
    field :id, Int, null: false
    field :title, String
    field :body, String
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
  end
end

Add the Types::ArticleType to QueryType as follows:

module Types
  class QueryType < Types::BaseObject
    field :article, type: Types::ArticleType, null: true, resolver: Resolvers::Article
  end
end
Write a comment

Conclution

Happy Hacking!!

Write a comment
この記事が気に入ったら応援お願いします🙏
4
ツイート
LINE
Developer
Price Rank Dev
I use Next.js (React) and Firebase (Firestore / Auth) for development. We are also developing APIs for Ruby on Rails and GraphQL. Our team members are 6 Vietnamese and Japanese engineers.