🔧

新卒エンジニアとインターン生が社内のREST APIをGraphQLに置き換えている話

2023/12/15に公開

これは 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化を行います。

移行手順

実際の移行手順は以下のようになります

  1. Railsで使用されているControllerを調査→仕様を確認して実装チケットを切る
  2. Resolverの実装をする。(backendのみリリース)
  3. Next.jsでGraphQLを使用した実装を行う(Frontendのリリース)
  4. 不要になった,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さんの記事を参考にしていただければと思います!(最近はこれらの階層にさらにドメインごとのサブディレクトリを置いていたりします)
https://zenn.dev/mybest_dev/articles/a8f3096821851c

GraphiQLで動作確認する

frontend側の設定になってしまいますが、GraphiQLというグラフィカルでインタラクティブなブラウザー内でGraphQL APIを叩いたり、スキーマを確認できる便利ツールがあります。
https://github.com/graphql/graphiql

動作確認が終わったQueryを使ってRequest TestをRspecで書きます。

マイベストでは、GraphQL自体のObjectTypeには、基本的にビジネスロジックはmodelやDecoratorに寄せるように実装を行い、最低限のレスポンスの振る舞いのみ書けば良いというルールでバックエンドテストのガイドラインなどを作成したりしています。
(詳しくは@isaka1022が書いた以下の記事に書いてあります!)
https://zenn.dev/mybest_dev/articles/619541f28de6d5

また、Frontendエンジニアがすぐに使えるように、Backend側でSchemaを定義した場合はgraphql-codegenを使って、自動生成のコードを作成します
https://the-guild.dev/graphql/codegen/plugins/typescript/typescript

ここまでできたら、一旦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の導入、移行を検討している方の参考になればと思います。

Discussion