新卒エンジニアとインターン生が社内のREST APIをGraphQLに置き換えている話
これは GraphQL Advent Calendar 15日目の記事です。
はじめに
23卒で株式会社マイベストに新卒入社したkatakyoです。バックエンドエンジニアとして、社内のバックエンドシステムのREST APIをGraphQLに移行する技術課題に取り組んでいます。この記事では、その手順や遭遇した問題点などをまとめています。
経緯
mybestでは、自分が新卒入社したタイミングでエンドユーザーに見えるシステムのBackendのGraphQL化、FrontendのNext.js化が終わっていましたが、管理画面や新規事業のFavlistのシステムでGraphQLとRailsのREST APIの通信が一部残っているような状況でした。
mybestにはミッションという事業課題の他に技術課題に20%時間を割くというルールがあり、その技術課題の中で新卒エンジニアが主体となってBackendのGraphQL化、FrontendのNext.js化を行います。
移行手順
実際の移行手順は以下のようになります
- Railsで使用されているControllerを調査→仕様を確認して実装チケットを切る
- Resolverの実装をする。(backendのみリリース)
- Next.jsでGraphQLを使用した実装を行う(Frontendのリリース)
- 不要になった,Railsのview、controller,routingを削除する
Railsで使用されているControllerを調査
社内のソースコードからcontrollerのファイルを特定し、そのcontrollerで使用されている機能を調査します。例えば、以下に示すUsersControllerがある場合、このcontrollerに紐づくviewファイルとroutingを確認します。
module Admin
class UsersController < ApplicationController
def index
end
def new
@user = User.new
end
def edit
@user = User.find(params[:id])
end
def update
@user = User.find(params[:id])
if @user.update(user_params)
redirect_to edit_admin_user_path(@user), notice: 'User was successfully updated.'
else
render :edit
end
end
private
def user_params
params.require(:user).permit(:name, :email)
end
end
end
上記のUsersControllerの場合、indexメソッドは一覧表示、newメソッドは新規作成画面の表示、editメソッドは編集画面の表示に対応しています。HTTPメソッドとの対応では、indexメソッドはGET、newメソッドは一般的にはGET(新規作成フォームの表示)、editメソッドもGET(編集フォームの表示)に対応します。
GraphQLでは、データ取得のためのQuery、データの変更(作成・更新・削除)を行うMutation、イベント監視のSubscriptionの3種類があります。REST APIやSQLとの対応を以下のように考えることができます。
操作 | REST | SQL | GraphQL |
---|---|---|---|
データ取得 | GET | Select | Query |
データ追加 | POST | Create | Mutation |
データ更新 | PATCH | Update | Mutation |
データ削除 | DELETE | Delete | Mutation |
イベント監視 | - | - | Subscription |
したがって、UsersControllerをGraphQLに置き換える場合、indexメソッドをQueryに、newメソッドとeditメソッドをそれぞれフォームの表示のためのQueryに、updateメソッドをMutationに置き換える必要があります。
Resolverの実装(backendのみリリース)
GraphQLでは、データ操作の実際を担う部分をResolverと呼びます。まず、対象となるRailsのControllerに紐づくモデルを参照して、GraphQLスキーマにおける型をObjectTypeとして定義します。もし引数が必要な場合、argumentを使用してその型と必須入力かどうかを指定します。引数が多数ある場合は、それらをInputTypeとして定義し、InputTypeごとに呼び出すことも可能です。例ではusers_controllerのupdateメソッドを実装する例を紹介します。
ObjectTypeの定義
上記のControllerの例に合わせて、Userモデルに対応するObjectTypeを定義します
module Admin
module ObjectTypes
class UserType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: true
field :email, String, null: true
end
end
end
InputTypeの定義
次に、ユーザー更新のための入力型UserUpdateInputを定義します。
module Admin
module InputTypes
class UpdateUserInputType < Types::BaseInputObject
argument :name, String, required: false
argument :email, String, required: false
end
end
end
Mutationの定義
実際のデータの更新の処理を書いていきます。errors内にはエラーメッセージを返すようにして、Frontend側では、errorsが空配列であれば処理が成功したと判定するようにしています。(保存成功のアラートはFrontendで書くようにします)
module Admin
class Mutations::UpdateUserMutation < Mutations::BaseMutation
argument :input, Admin::InputTypes::UpdateUserInput, required: true
field :user, Admin::ObjectTypes::UserType, null: false
field :errors, [String], null: false
def resolve(**args)
params = args[:update_user_input].to_h
user = User.find(params[:id])
if user.update(params)
{
user: user,
errors: []
}
else
{
user: nil,
errors: user.errors.full_messages
}
end
end
end
end
ObjectTypeにMutationのFieldを定義
実際に、Mutationを使えるようにするためにOnjectTypesのMutationTypeに実装したmutationのfieldを用意します。QueryのResplverを実装した場合はQueryTypeのObjectTypeを用意し、それぞれのドメインに合わせて名前空間を分けて整理しています。
module Admin
module ObjectTypes
class MutationType < BaseObject
field update_user, mutation: Admin::Mutations::UpdateUserMutation
end
end
end
この例では、ユーザー更新のための引数としてUpdateUserInputを用いており、更新後のユーザー情報はUserTypeで定義された形式で返されます。このようにInputTypeとObjectTypeを使うことで、GraphQLのスキーマがより整理され、理解しやすくなります。また、入力と出力の型が明確になるため、APIの利用者にとっても使用しやすくなります。
弊社のファイル・ディレクトリ構成などはegamiTaさんの記事を参考にしていただければと思います!(最近はこれらの階層にさらにドメインごとのサブディレクトリを置いていたりします)
GraphiQLで動作確認する
frontend側の設定になってしまいますが、GraphiQLというグラフィカルでインタラクティブなブラウザー内でGraphQL APIを叩いたり、スキーマを確認できる便利ツールがあります。
動作確認が終わったQueryを使ってRequest TestをRspecで書きます。
マイベストでは、GraphQL自体のObjectTypeには、基本的にビジネスロジックはmodelやDecoratorに寄せるように実装を行い、最低限のレスポンスの振る舞いのみ書けば良いというルールでバックエンドテストのガイドラインなどを作成したりしています。
(詳しくは@isaka1022が書いた以下の記事に書いてあります!)
また、Frontendエンジニアがすぐに使えるように、Backend側でSchemaを定義した場合はgraphql-codegenを使って、自動生成のコードを作成します
ここまでできたら、一旦Backendのみリリースします
Frontendを実装する
Frontend側でBackendで定義したGraphQL Schemaを使ってFrontendの実装を行います。マイベストではRailsのslimからNext.jsへの置き換えも行っているため、Frontendの置き換えが完了したらnginxの設定でNext.jsの画面にアクセスするようにしています
不要になったファイルを削除する
GrapQLとFrontend側の置き換えが終わったので、controllerやRailsのviewファイル、ルーティングは不要になるので、削除します。削除を並行してやる場合、pathなど別の画面で使われているか注意してgrepしながら行います。
移行中に詰まったこと
実装前の調査が大変だった
自分が技術課題の担当をメインでやる前にすでに、大部分のGraphQLの移行は完了していましたが、残りのAPIがどの程度存在しているのか見えていない状況でした。実装当初はとりあえずタスクを洗い出してできそうな画面からやっていたのですが、途中からインターン生の加入などメンバーが増えてきたので改めて全てのタスクの調査をやり直し、進捗などを可視化できるようにNotionやJiraを使ってプロジェクト管理をしてタスクを渡せるような体制にしました。
依存関係のあるコントローラーやviewの削除時に障害を出した
FrontendとBackendの両方の置き換えが終わったタイミングで不要コードになったコードを削除しても置き換えが途中の画面で_path
のようなヘルパーメソッドが使われており、ルーティングを消したタイミングでエラーになってしまうことがありました。レビュー環境や開発環境の段階で気づければ良いのですが、Githubの差分では気付けないため、チケット発行時の調査を入念にしたり、削除時のガイドラインなどを作成し対応しています。
まとめ
今回はRailsアプリケーションで動いている社内のバックエンドシステムのREST APIをGraphQLに移行している事例に関して紹介しました。
RailsアプリケーションからGraphQLの導入、移行を検討している方の参考になればと思います。
株式会社マイベストのテックブログです! 採用情報はこちら > notion.so/mybestcom/mybest-information-for-Engineers-8beadd9c91ef4dc2b21171d48a4b0c49
Discussion