GraphQL API with Rails and MongoDB


For the backend part of the coffeekzn.ru web app I decided to choose noSQL database MongoDB and Mongoid instead of ActiveRecord framework.

Let’s create sample Rails API project without ActiveRecord for example:

 rails new coffeekzn_api --api --skip-bundle --skip-active-record --skip-test --skip-system-test

Great, let’s add mongoid and graphql gems and some other extensions:

gem 'mongoid', '~> 6.2.1'
gem 'bson_ext'
gem 'graphql'
gem 'graphql-batch'
gem 'graphql-preload'
gem 'mongoid_rails_migrations'
gem 'geocoder'
gem 'mongoid-slug'
gem 'mongoid-tags'

Great, now we need to create config/mongoid.yml file and can go forward.

development:
  clients:
    default:
      database: coffeekzn_api_development
      hosts:
        - 127.0.0.1:27017
test:
  clients:
    default:
      database: coffeekzn_api_test
      hosts:
        - 127.0.0.1:27017
      options:
        read:
          mode: :primary
        max_pool_size: 1

Let’s create shop.rb model:

class Shop
  include Mongoid::Document
  include Mongoid::Timestamps
  include Geocoder::Model::Mongoid
  include Mongoid::Slug
  include Mongoid::Tags

  field :email, type: String
  field :title, type: String
  field :description, type: String
  field :address, type: String
  field :preview_image, type: String
  field :coordinates, type: Array

  field :vk, type: String
  field :facebook, type: String
  field :twitter, type: String
  field :instagram, type: String
  field :website, type: String

  field :working_hours, type: String
  field :phone, type: String

  field :espresso_price, type: String
  field :cappuccino_price, type: String
  field :roasting, type: String
  field :features, type: String
  field :coffee_machine, type: String
  field :sell_in_beans, type: Boolean, default: false
  field :alternate, type: Boolean, default: false
  field :merchandise, type: Boolean, default: false

  slug :title

  geocoded_by :address
  after_validation :geocode

  scope :ordered, -> { order('created_at DESC') }

  validates :title, presence: true
  validates :slug, presence: true, uniqueness: true
end

Nice one. Now, we can create our models from rails console for example, as usual:

irb(main):015:0> Shop.new
=> #<Shop _id: 5a69c62cd7849c31286c9660, created_at: nil, updated_at: nil, tags: [], email: nil, title: nil, description: nil, address: nil, preview_image: nil, coordinates: nil, vk: nil, facebook: nil, twitter: nil, instagram: nil, website: nil, working_hours: nil, phone: nil, espresso_price: nil, cappuccino_price: nil, roasting: nil, features: nil, coffee_machine: nil, sell_in_beans: false, alternate: false, merchandise: false, _slugs: nil>

Great! Let’s move to our API implementation using GraphQL!

Nothing special here, you can read about basic GraphQL setup in on of my previous posts: GraphQL + Rail.

Let’s take a look into shop_type.rb implementation:

Types::ShopType = GraphQL::ObjectType.define do
  name 'Shop'

  field :id, !types.ID
  field :email, types.String

  field :instagram, types.String
  field :vk, types.String
  field :facebook, types.String
  field :twitter, types.String
  field :website, types.String
  field :phone, types.String

  field :title, !types.String
  field :description, !types.String
  field :working_hours, !types.String
  field :address, !types.String
  field :preview_image, types.String
  field :espresso_price, types.String
  field :cappuccino_price, types.String
  field :roasting, types.String
  field :features, types.String
  field :coffee_machine, types.String
  field :sell_in_beans, types.Boolean
  field :alternate, types.Boolean
  field :merchandise, types.Boolean
  field :tags, types[types.String]

  field :slug, types.String

  field :location do
    type Types::LocationType
    resolve -> (obj, args, ctx) {
      obj.coordinates
    }
  end

  field :updated_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.updated_at.to_i
    }
  end

  field :created_at do
    type types.Int

    resolve -> (obj, args, ctx) {
      obj.created_at.to_i
    }
  end
end

And the query_type.rb file:

Types::QueryType = GraphQL::ObjectType.define do
  name "Query"
  field :shops, !types[Types::ShopType] do
    resolve -> (obj, args, ctx) {
      Shop.order(title: 'asc')
    }
  end
  field :shop do
    type Types::ShopType
    argument :id, !types.ID
    description "Find a Shop by SLUG"
    resolve ->(obj, args, ctx) { Shop.find(args["id"]) }
  end
end

Great, let’s check how is our API working on all collection:

GQ1

And getting one Shop by ID:

GQ2

Enjoy :)