[Awesome Ruby Gem - Graphiti] Use graphiti-rails gem to provide RESTful API

graphiti-rails

Graphiti makes RESTful Resources a first-class concept. This enables reading and writing a graph of data in a single request, a schema with backwards-compatible guarantee, end-to-end integration test patterns, seamless microservices and much more.

This quickstart will use Rails with ActiveRecord to give an overview of Graphiti functionality on familiar ground. For a more in-depth breakdown, head to the Guides - https://www.graphiti.dev/guides.

Installation

You can install it as a gem:

1
$ gem install graphiti-rails

or add it into a Gemfile (Bundler):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Gemfile

# graphiti-api/graphiti-rails
# https://github.com/graphiti-api/graphiti-rails
gem 'graphiti-rails'

# For automatic ActiveRecord pagination
gem 'kaminari'

# Test-specific gems
group :development, :test do
gem 'rspec-rails'
gem 'factory_bot_rails'
gem 'faker'
gem 'graphiti_spec_helpers'
end

group :test do
gem 'database_cleaner'
end

Then, run bundle install.

1
$ bundle install

Usages

Defining a Model

First, let’s define our Model:

1
2
3
$ bundle exec rails generate model Post title:string upvotes:integer active:boolean

$ bundle exec rails db:migrate

Config

The .graphiticfg.yml file lives in the root directory of your application. It holds configuration we need to reuse across a variety of contexts (primarily generates and rake tasks). If you use our template to create your application, it’s created for you.

Primarily this is used to hold your “API namespace”:

1
2
3
4
# .graphiticfg.yml

---
namespace: /api/v1

If this file doesn’t exist you may get unexpected errors - make sure to create it!


Defining a Resource

A Resource defines how to query and persist your Model. In other words: a Model is to the database as Resource is to the API.

Now we can use the built-in generator to define our Resource, corresponding Endpoint, and Integration Tests.

1
2
3
4
5
6
7
8
9
10
11
$ bundle exec rails g graphiti:resource Post title:string upvotes:integer active:boolean
create app/controllers/api/v1/posts_controller.rb
modify config/routes.rb
create app/resources/post_resource.rb
create spec/resources/post/writes_spec.rb
create spec/resources/post/reads_spec.rb
create spec/api/v1/posts/index_spec.rb
create spec/api/v1/posts/show_spec.rb
create spec/api/v1/posts/create_spec.rb
create spec/api/v1/posts/update_spec.rb
create spec/api/v1/posts/destroy_spec.rb

Check generated code

PostsController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# app/controllers/api/v1/posts_controller.rb

module Api
module V1
class PostsController < ApplicationController
def index
posts = PostResource.all(params)
respond_with(posts)
end

def show
post = PostResource.find(params)
respond_with(post)
end

def create
post = PostResource.build(params)

if post.save
render jsonapi: post, status: 201
else
render jsonapi_errors: post
end
end

def update
post = PostResource.find(params)

if post.update_attributes
render jsonapi: post
else
render jsonapi_errors: post
end
end

def destroy
post = PostResource.find(params)

if post.destroy
render jsonapi: { meta: {} }, status: 200
else
render jsonapi_errors: post
end
end
end
end
end

ApplicationController

1
2
3
4
5
# app/controller/application_controller.rb

class ApplicationController < ActionController::API
include Graphiti::Rails::Responders
end

Route

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# config/routes.rb

Rails.application.routes.draw do
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

# Defines the root path route ("/")
# root "articles#index"

namespace :api do
namespace :v1 do
resources :posts
end
end
end

ApplicationResource

1
2
3
4
5
6
7
8
9
10
11
12
13
# app/resources/application_resource.rb

# ApplicationResource is similar to ApplicationRecord - a base class that
# holds configuration/methods for subclasses.
# All Resources should inherit from ApplicationResource.
class ApplicationResource < Graphiti::Resource
# Use the ActiveRecord Adapter for all subclasses.
# Subclasses can still override this default.
self.abstract_class = true
self.adapter = Graphiti::Adapters::ActiveRecord
self.base_url = Rails.application.routes.default_url_options[:host]
self.endpoint_namespace = '/api/v1'
end

PostResource

1
2
3
4
5
6
# app/resources/post_resource.rb

class PostResource < ApplicationResource
attribute :name, :string

end

Call PostResource built-in methods:

1
2
3
4
5
6
$ rails c
irb(main):001:0> PostResource.all.data # => [#<Post>, #<Post>, ...]

irb(main):001:1> PostResource.all.to_json

irb(main):001:2> PostResource.all.to_json

Run

You’ll see a number of files created. Now run your app!:

1
$ bundle exec rails s

Verify http://localhost:3000/api/v1/posts renders JSON correctly. Now we just need data.

Seeding Data

Edit db/seeds.rb to create a few Posts:

1
2
3
Post.create!(title: 'My title', upvotes: 10, active: true)
Post.create!(title: 'Another title', upvotes: 20, active: false)
Post.create!(title: 'OMG! A title', upvotes: 30, active: true)

And run the script:

1
$ bundle exec rails db:seed

Now load http://localhost:3000/api/v1/posts. You should have 3 Posts in your database!

FAQs

undefined method `delete_suffix’ for nil:NilClass (NoMethodError)

1
2
3
$ bundle exec rails g graphiti:resource Post title:string upvotes:integer active:boolean
create app/controllers/posts_controller.rb
/Users/cloudolife/.asdf/installs/ruby/3.0.3/lib/ruby/gems/3.0.0/gems/graphiti-1.3.5/deprecated_generators/graphiti/generator_mixin.rb:15:in `api_namespace': undefined method `delete_suffix' for nil:NilClass (NoMethodError)

File .graphiticfg.yml doesn’t exist you may get unexpected errors - make sure to create it!

1
2
3
4
# .graphiticfg.yml

---
namespace: /api/v1

File unchanged! The supplied flag value not found! config/routes.rb

1
2
$ bundle exec rails g graphiti:resource Post title:string upvotes:integer active:boolean
File unchanged! The supplied flag value not found! config/routes.rb

TODO

uninitialized constant ApplicationResource (NameError)

1
2
3
4
$ bundle exec rails g graphiti:resource Post title:string upvotes:integer active:boolean
create app/resources/post_resource.rb
/Users/cloudolife/graphiti-rails-demo/app/resources/post_resource.rb:1:in `<main>': uninitialized constant ApplicationResource (NameError)
Did you mean? ApplicationRecord

Try to run bundle exec rails g graphiti:resource Post title:string upvotes:integer active:boolean again to fix that issue.

1
$ bundle exec rails g graphiti:resource Post title:string upvotes:integer active:boolean

NoMethodError (undefined method `respond_with’

1
2
NoMethodError (undefined method `respond_with' for #<Restful::Api::V1::DataDictionariesController:0x000000000131a0>
Did you mean? respond_to?):

Append include Graphiti::Rails::Responders into file application_controller.rb.

1
2
3
4
5
# app/controller/application_controller.rb

class ApplicationController < ActionController::API
+ include Graphiti::Rails::Responders
end

Could not find type :jsonb! This was specified on attribute :annotations within resource DataDictionaryResource (Graphiti::Errors::TypeNotFound)

1
2
$ RAILS_ENV=production rails c 
~/.asdf/installs/ruby/3.0.3/lib/ruby/gems/3.0.0/gems/graphiti-1.3.5/lib/graphiti/resource/dsl.rb:127:in `attribute': Could not find type :jsonb! This was specified on attribute :annotations within resource DataDictionaryResource (Graphiti::Errors::TypeNotFound)

TODO

/usr/local/lib/ruby/3.0.0/debug.rb:6:in <main>': undefined method >’ for nil:NilClass (NoMethodError)

1
2
3
4
5
6
# /usr/local/lib/ruby/3.0.0/debug.rb

if $SAFE > 0
STDERR.print "-r debug.rb is not available in safe mode\n"
exit 1
end

References

[1] Stylish Graph APIs | Guides - https://www.graphiti.dev/guides/

[2] graphiti-api/graphiti: Stylish Graph APIs - https://github.com/graphiti-api/graphiti

[3] Migrating to graphiti-rails - https://www.graphiti.dev/guides/graphiti-rails-migration

[4] graphiti-api/graphiti-rails - https://github.com/graphiti-api/graphiti-rails

[5] graphiti-rails | RubyGems.org | your community gem host - https://rubygems.org/gems/graphiti-rails/

[6] JSON:API — A specification for building APIs in JSON - https://jsonapi.org/

[7] JSON:API — Implementations - https://jsonapi.org/implementations/