REST API to GraphQL migration strategy - preparatory work ( Ruby on Rails )

Sommaire
Summary
This article follows the previous one, on the theme of migrating a REST API to GraphQL.
In the previous article, we reviewed the action plan put in place to carry out this migration.
In this post we will look at the preparation phase in more detail. First, a short reminder:
Preparatory work
- List the controllers & actions that will be present on the GraphQL version of the API.
- Extract controller actions and transform them into objects (design pattern command).
- Optional (but highly recommended !) : update your test coverage.
List the controllers & actions that will be present on the GraphQL version of the API
For this phase, only you can list which actions are used or not by your API's consumers.
For the methodology, an exhaustive pass over all controllers is necessary.
You can also take the opportunity to take notes on refactor work to be planned, unit tests to write... This will allow you to start estimating more precisely the time required for future steps.
Extract controller actions and transform them into objects
I recommend first familiarizing yourself with the Command (or Service Object ) design pattern
The goal of this step is to extract each action from your controllers into an object. By proceeding in this way, several advantages emerge:
- You will be able to identify the dependencies of your code ;
- You will be able to test the code logic more easily, independently of the controller context ;
- You will be able to reuse your code in multiple places ;
In the following example, we will extract the logic of the create action in the posts_controller.rb controller.
Before refactor:
## app/api/v1/posts_controller.rb
module Api
module V1
class PostsController < ::Api::ApplicationController
def create
## Logique métier
## ...
## ...
if post_repository.save(post)
redirect_to post_path(post)
else
render "new"
end
end
def post_repository
@post_repository ||= ::Repositories::Post.new
end
end
end
end
After refactor:
## lib/command/post/create.rb
module Command
module Post
class Create
def self.exec({ attrs, callbacks, repositories })
new(callbacks, repositories).exec(attrs)
end
def exec(attrs)
## Logique métier
## ...
## ...
if @repositories[:post].save(post)
@callbacks[:success].call(post: post)
else
@callbacks[:failure].call(post: post)
end
end
private
def initialize(callbacks, repositories)
@callbacks = callbacks
@repositories = repositories
end
end
end
end
## app/api/v1/posts_controller.rb
module Api
module V1
class PostsController < ::Api::ApplicationController
def create
::Command::Post::Create.exec(
{
attrs: params,
callbacks: {
success: ->(args) {
@post = args[:post]
redirect_to post_path(@post)
},
failure: ->(args) {
@post = args[:post]
render "new"
},
},
repositories: {
post: ::Repositories::Post.new,
}
}
)
end
end
end
end
Here are some links that may be useful to go further:
- https://www.codeproject.com/Articles/12263/The-Command-Pattern-and-MVC-Architecture
- https://blog.slava.dev/thin-controllers-fat-models-one-of-the-base-principles-of-mvc-in-ruby-on-rails-but-time-goes-by-a5a044124207
- https://www.honeybadger.io/blog/refactor-ruby-rails-service-object/
- https://www.cloudbees.com/blog/refactoring-legacy-rails-controllers
Update your test coverage
Before any modification or refactor, I recommend writing & updating your tests. This will allow you to "break everything" with peace of mind!
I will not dwell on setting up unit tests, but I will present an example adapted to the changes.
If you use rspec, the example below can serve as a template to test your Command objects.
require 'rails_helper'
RSpec.describe ::Command::Post::Create do
let (:callbacks) {
{
success: -> (*args) { :success },
failure: -> (*args) { :failure }
}
}
before do
post_repository = double('::Repositories::Post')
allow(post_repository).to receive(:save).with(an_instance_of(::Post)).and_return(true)
@repositories = {
post: post_repository,
}
end
let (:command_post_create) { ::Command::Post::Create }
describe "create" do
context "the record is created" do
let (:attrs) {
{
title: "Test"
}
}
it "calls success callback" do
result = command_post_create.exec(
attrs: attrs,
callbacks: callbacks,
repositories: @repositories
)
expect(result).to eq :success
end
end
end
end
Here are some links that may be useful to go further:
Conclusion
After this work, you should have separated business logic from controller actions into specific objects. These have their dependencies well identified and can be properly tested.
You can now use these objects anywhere in your project, which will avoid duplicating code between your two API versions.
You therefore now have a good added value in terms of clarity and structure!
Comments
Loading...