Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
既存のRESTful な RailsプロジェクトにGraphQL導入を検討した話
Search
hiroya iizuka
September 24, 2021
5.1k
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
既存のRESTful な RailsプロジェクトにGraphQL導入を検討した話
hiroya iizuka
September 24, 2021
More Decks by hiroya iizuka
See All by hiroya iizuka
学問と資産
hiroyaiizuka
0
510
clean architecture と経営
hiroyaiizuka
0
1.5k
3つのNext.jsプロジェクトを 新卒エンジニアと一緒に 開発した話
hiroyaiizuka
1
470
ReactNative + microCMS の設計にすごく悩んだ話
hiroyaiizuka
1
1.2k
実戦に役立つFirebase_Analytics_使用集
hiroyaiizuka
0
180
Featured
See All Featured
brightonSEO & MeasureFest 2025 - Christian Goodrich - Winning strategies for Black Friday CRO & PPC
cargoodrich
3
750
Why You Should Never Use an ORM
jnunemaker
PRO
61
9.9k
Building Flexible Design Systems
yeseniaperezcruz
330
40k
Between Models and Reality
mayunak
4
370
Keith and Marios Guide to Fast Websites
keithpitt
413
23k
How To Stay Up To Date on Web Technology
chriscoyier
790
250k
The AI Search Optimization Roadmap by Aleyda Solis
aleyda
1
6k
The agentic SEO stack - context over prompts
schlessera
0
850
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
333
23k
Designing for Timeless Needs
cassininazir
1
370
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
659
62k
Abbi's Birthday
coloredviolet
3
8.7k
Transcript
既存のRESTful な Rails プロジェクトに GraphQL 導⼊を検討した話 株式会社BeatFit CTO 飯塚 浩也
医師 循環器内科、総合診療医と して、⼤学 - 地⽅の病院を 8 年間勤務。 IT は⼤の苦⼿。 パソコン教室のアビバに
通っていた。 エンジニア 株式会社BeatFit にエンジニ アとして⼊社。 [ ⽇課] Codewars, LeetCode Hack The Box Type Challenge
None
今⽇のお話 ⼩規模エンジニア組織の課題 課題解決を⽬指した技術選定と現実とのギャップ 最終的にどんな教訓がえられたか? ⼩規模なエンジニア組織での 技術選定に参考になれば 🙏
うおおおお 〜! Twitter でインターン研修の連絡
内定のご連絡
翌⽇
レガシーな技術選定のせい・・・? Rxjs, Redux, REST API...
弊社エンジニアチームの歴史 2018 年創業 外部エンジニア2 名(frontend, backend) で開発スタート その後 React Native
エンジニア4 名 Rails エンジニア2 名 2020 年、資⾦繰りが⼀時悪化し 正社員エンジニア1 名、業務委託エンジニア2 名に・・・
技術スタック
Frontend
課題1. Redux による状態管理 Text Technology Radar
import { ajax } from 'rxjs/ajax'; const fetchUserEpic = (action$,
state$) => action$.pipe( ofType('FETCH_USER'), mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe( map(response => ({ type: 'FETCH_USER_FULFILLED', payload: response })) ) ); 1 2 3 4 5 6 7 8 9 10 11 ・コードが複雑で、Rxjs の学習コストが⾼い ・API response に型がつかない 課題2. Redux Observable
import { ajax } from 'rxjs/ajax'; const fetchUserEpic = (action$,
state$) => action$.pipe( ofType('FETCH_USER'), mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe( map(response => ({ type: 'FETCH_USER_FULFILLED', payload: response })) ) ); 1 2 3 4 5 6 7 8 9 10 11 mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe( import { ajax } from 'rxjs/ajax'; 1 2 const fetchUserEpic = (action$, state$) => action$.pipe( 3 ofType('FETCH_USER'), 4 5 map(response => ({ 6 type: 'FETCH_USER_FULFILLED', 7 payload: response 8 })) 9 ) 10 ); 11 ・コードが複雑で、Rxjs の学習コストが⾼い ・API response に型がつかない 課題2. Redux Observable
import { ajax } from 'rxjs/ajax'; const fetchUserEpic = (action$,
state$) => action$.pipe( ofType('FETCH_USER'), mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe( map(response => ({ type: 'FETCH_USER_FULFILLED', payload: response })) ) ); 1 2 3 4 5 6 7 8 9 10 11 mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe( import { ajax } from 'rxjs/ajax'; 1 2 const fetchUserEpic = (action$, state$) => action$.pipe( 3 ofType('FETCH_USER'), 4 5 map(response => ({ 6 type: 'FETCH_USER_FULFILLED', 7 payload: response 8 })) 9 ) 10 ); 11 map(response => ({ import { ajax } from 'rxjs/ajax'; 1 2 const fetchUserEpic = (action$, state$) => action$.pipe( 3 ofType('FETCH_USER'), 4 mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe( 5 6 type: 'FETCH_USER_FULFILLED', 7 payload: response 8 })) 9 ) 10 ); 11 ・コードが複雑で、Rxjs の学習コストが⾼い ・API response に型がつかない 課題2. Redux Observable
import { ajax } from 'rxjs/ajax'; const fetchUserEpic = (action$,
state$) => action$.pipe( ofType('FETCH_USER'), mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe( map(response => ({ type: 'FETCH_USER_FULFILLED', payload: response })) ) ); 1 2 3 4 5 6 7 8 9 10 11 mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe( import { ajax } from 'rxjs/ajax'; 1 2 const fetchUserEpic = (action$, state$) => action$.pipe( 3 ofType('FETCH_USER'), 4 5 map(response => ({ 6 type: 'FETCH_USER_FULFILLED', 7 payload: response 8 })) 9 ) 10 ); 11 map(response => ({ import { ajax } from 'rxjs/ajax'; 1 2 const fetchUserEpic = (action$, state$) => action$.pipe( 3 ofType('FETCH_USER'), 4 mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe( 5 6 type: 'FETCH_USER_FULFILLED', 7 payload: response 8 })) 9 ) 10 ); 11 payload: response import { ajax } from 'rxjs/ajax'; 1 2 const fetchUserEpic = (action$, state$) => action$.pipe( 3 ofType('FETCH_USER'), 4 mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe( 5 map(response => ({ 6 type: 'FETCH_USER_FULFILLED', 7 8 })) 9 ) 10 ); 11 ・コードが複雑で、Rxjs の学習コストが⾼い ・API response に型がつかない 課題2. Redux Observable
import { ajax } from 'rxjs/ajax'; const fetchUserEpic = (action$,
state$) => action$.pipe( ofType('FETCH_USER'), mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe( map(response => ({ type: 'FETCH_USER_FULFILLED', payload: response })) ) ); 1 2 3 4 5 6 7 8 9 10 11 mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe( import { ajax } from 'rxjs/ajax'; 1 2 const fetchUserEpic = (action$, state$) => action$.pipe( 3 ofType('FETCH_USER'), 4 5 map(response => ({ 6 type: 'FETCH_USER_FULFILLED', 7 payload: response 8 })) 9 ) 10 ); 11 map(response => ({ import { ajax } from 'rxjs/ajax'; 1 2 const fetchUserEpic = (action$, state$) => action$.pipe( 3 ofType('FETCH_USER'), 4 mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe( 5 6 type: 'FETCH_USER_FULFILLED', 7 payload: response 8 })) 9 ) 10 ); 11 payload: response import { ajax } from 'rxjs/ajax'; 1 2 const fetchUserEpic = (action$, state$) => action$.pipe( 3 ofType('FETCH_USER'), 4 mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe( 5 map(response => ({ 6 type: 'FETCH_USER_FULFILLED', 7 8 })) 9 ) 10 ); 11 import { ajax } from 'rxjs/ajax'; const fetchUserEpic = (action$, state$) => action$.pipe( ofType('FETCH_USER'), mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe( map(response => ({ type: 'FETCH_USER_FULFILLED', payload: response })) ) ); 1 2 3 4 5 6 7 8 9 10 11 ・コードが複雑で、Rxjs の学習コストが⾼い ・API response に型がつかない 課題2. Redux Observable
・創業当初、API ドキュメントがなく、⽕種が勃発 ・2019 年、Api client に Postman を導⼊ ・2020 年、Swagger
を導⼊ 課題3. REST API 資⾦難により、frontend, backend を1 ⼈で担当。 開発スピード低下を懸念し、Swagger のメンテナンスしなくなる😓
backend
課題1. REST API の問題 ・endpoint の命名に悩む 規則がなく統⼀感がない ( 単数形と複数形が混在しているなど) ・増⼤していく⼤量のAPI
達 (v1, v2, v3...)
課題2. controller, View の問題 ・restful な設計に、⼀部なっていない 独⾃で定義されているaction が多い ・jbuilder にfrontend
で使われないデータが混⼊する # app/controllers/api/v1/user_controller.rb class Api::V1::UserController < ApiController def create ... end def web_registration ... end end 1 2 3 4 5 6 7 8 9 10 11 12
課題3. frontend とのコミュニケーションの問題 ・クライアント都合( ちょっとした変更) で、サーバーサ イドに依頼して実装を修正するという煩わしさ ・API ドキュメントの管理コストが⼤きい ・frontend
は、API の実装もしくは、mock server の作 成を待たないと、実装が進められない
課題まとめ ☑ リリースから3 年が経過し、リファクタリング、API ドキュメントの メンテンスなどの、保守運⽤コストが増⼤してきた。 ☑ frontend では、時の流れとともにbest practice
がかわってきた。 を許容できなくなってきた。 ☑ backend では、REST 特有の問題である などが問題になってきた。 ユースケース毎のAPI 実装の煩わしさ API ドキュメント管理の煩わしさ コミュニケーションコスト増加 API の実装を待たないと実装ができない 型がないなどの開発体験
API endpoint 名で悩みたくない これ以上API endpoint 増やしたくない API ドキュメント管理煩わしい・・・! 型安全なSchema driven
な開発をしたい frontend の開発時期を遅らせたくない GraphQL を導⼊しよう!
エンジニア3 ⼈でTry したこと A さん: Frontend, GraphQL 経験++ B さん:
Backend, GraphQL 経験- わたし: Frontend, Backend, GraphQL 経験+/- A B
1. 会議 GraphQL っていいですよね! キャッチアップ よろしくお願いいたします!
2. 設計の検討 BFF
BFF (backend for frontend) フロントエンドとバックエンドの中間に位置 するサーバーを置くアーキテクチャ
API request 結婚⽣活
必要なrequest に個別に答えてくれる ⼀度やったことを覚えている マニュアルを作ってれる
Frontend Backend 関⼼ 関⼼
メリット Frontend は ServerSide の実装に 依存しない 型がつく Redux 廃⽌できる (Apollo
cache) overfetch 発⽣しない Server Side の実装が Frontend の細かい要求に 引っ張られなくなる API endpoint の悩みなし API ドキュメント管理不要 API の結果はキャッシュされ Frontend からのリクエスト削減
デメリット ・単⼀障害点 → BFF が何らかの理由で正常に機能しない場合、サービス全体に 影響が出る。 ・監視対象の増加と、デプロイの懸念事項の増加 → メンテナンスコスト増加 ・BFF
⾃体の実装負荷
コンウェイの法則からみた BFF 導⼊のタイミング システム設計( アーキテクチャ) は、組織構造 を反映させたものになる
組織は分離されるけどいいの? ⼈数少ないのに、疎な関係が正義なの? 横断的に開発している⼈はどうなる? コンウェイの法則からみた BFF 導⼊のタイミング システムの分離 = 組織の分離
各チームが独⽴した意思決定できるよう 他チームとの依存関係を断ち切る必要が あると判断される時 コンウェイの法則からみた BFF 導⼊のタイミング
Graphql Ruby の導⼊
REST → GraphQL への移⾏
なんとか、動くようになったが
消えぬ不安 ・エラーハンドリング設計 ・Schema 設計 ・Log 設計 ・N + 1 問題
etc ベストプラクティスが何もわからない・・・ 社内に知⾒を持ったエンジニアが不在で、⼿探りな状況 本番運⽤に耐えうる? ⾼速に開発できる???
最終的なジャッジ GraphQL 導⼊⾒送り😓
API endpoint 名で悩みたくない これ以上API endpoint 増やしたくない API ドキュメント管理煩わしい・・・! 型安全なSchema driven
な開発をしたい frontend の開発時期を遅らせたくない この2 つを対応しました
Schema 駆動開発 実装前に、wiki 上で schema とresponse の議論
得られた教訓 ⼩規模スタートアップで技術選定を決める時 組織構造に思いを馳せることが⼤事
エンジニアの⼈員が少ない コミュニケーションコストはまだ許容範囲内 サービスはまだスケールしていない サービス数がまだ少ない お互いが依存した密結合で value を発揮している組織のうちは 疎結合を促し組織を分割させてしまう技術選定は 時期尚早かも
リソース不⾜な状況では 社内に⼗分な経験をもったエンジニアがいない 新しめの技術選定は悪⼿になる 無理をせず、ビジネスの成⻑を優先し リソース確保ができるまで耐える 得られた教訓
まとめ 保守運⽤の課題を抱えた⼩規模スタートアップで GraphQL を導⼊しようと奮闘した しかし、導⼊によるシステムの成⻑が組織の成⻑に おいつかず、ビジネスへの悪影響が懸念された 苦しい時は無理をせず、ビジネスの成⻑とリソースの確保 を待ってから、攻めに転じるタイミングの⾒極めが重要 と思われた