Slide 1

Slide 1 text

GoでOpenAPIを導入して自動生成! 作成者:えばた あや 2022/11/18

Slide 2

Slide 2 text

自己紹介 株式会社Alumnote えばた あや ◆プロフィール ● 株式会社AlumnoteでバックエンドをGoで書いています! ● ラーメン二郎が好きです!

Slide 3

Slide 3 text

会社紹介 国内大学・教育機関の財政難を解決するために、 大学の財務構造(資金調達手段)のアップデートを目指しているスタートアップです。 大学支援者ネットワークのデータベース化〜拡大活性化、寄付募集の最大化を実現する Vertical SaaSの開発・提供と大学実務のハンズオン支援を行っています。 エンジニアメンバーがまだ少ないため、 ごーふぁー荘にて社内勉強会を開催することになりました!

Slide 4

Slide 4 text

こんな感じのサービス作ってます CRMツール 決済基盤 ・エクセルなど複数のシステムに分散  している卒業生・寄付者名簿を  名寄せ・統合して管理 ・名簿情報をさまざまな項目で簡単に  フィルタリングして抽出 ・HTMLメールをパソコン画面上で  簡単に作成し、名簿から配信可能 ・配信したメールの到達率、開封率など  の分析機能も標準で提供 ・寄付決済、会費決済向けの様々な  決済手段をワンストップで提供 ・決済情報は名簿情報と自動的に連携 大学支援者・関係者の名寄せ〜名簿管理 寄付マーケティングに必要なツール群 寄付者のデータが名簿に 自動連携される決済ツール コミュニティ ・卒業生検索・キャリアサービスなど  在校生や卒業生への便益提供が可能 ・名簿情報と自動連携されており、  最新の個人情報に更新できる 在校生・卒業生ネットワークの アクティベーション

Slide 5

Slide 5 text

目次 2 OpenAPIを使うことにした背景 3 Alumnoteではどんな使い方をしているか 4 まとめ 1 OpenAPIとは?

Slide 6

Slide 6 text

OpenAPI - RESTful APIのインターフェースを定義するための言語 - OpenaAPIを使うことで人間とコンピュータの両方が理解しやすくなる - OpenAPIを使用することで… - GUIでドキュメントを表示することができる - サーバーとクライアントコードを生成できる  など、いろんな使い方がある Swagger - OpenAPIを使ってRESTful APIを設計するためのツールセットのこと - エディタ、ドキュメントの表示、生成などをするためのツールが提供されている OpenAPIとは?

Slide 7

Slide 7 text

- スキーマ駆動ができる! - バックエンドとフロントエンドで意思疎通しやすい!! - めちゃくちゃ恩恵受けれた - バックエンドだけでなくフロントエンドも自動生成できる! - ドキュメントとして使うだけではない!! - 十分にプロダクトで使えるソースコードを生成できる - mockサーバを立ち上げられる - SDKなどを使って別リポジトリにすると言語依存がある - OpenAPIなら言語関係なくHTTPでやり取りできるインターフェースが書ける - クライアントコードも生成できるので書きやすい OpenAPIを使うことにした背景

Slide 8

Slide 8 text

こんな感じで使ってる Alumnoteではどんな使い方をしているか

Slide 9

Slide 9 text

こんな感じで使ってる Alumnoteではどんな使い方をしているか

Slide 10

Slide 10 text

ドキュメントを書くためのエディタ - Swagger Editor - https://editor.swagger.io/ - Goland - OpenAPI Specificationのプラグインを用いて ドキュメントの表示 - 現状サーバにあげたりしてないのでエディタのプレビュー機能で表示 使用ツール

Slide 11

Slide 11 text

Goのソースコード生成 - deepmap/oapi-codegen - https://github.com/deepmap/oapi-codegen - 以下のフレームワークを使用できる - Echo <- Alumnoteではこれを使ってる - Chi - Gin - net/http 使用ツール

Slide 12

Slide 12 text

Alumnoteの場合 - Dockerで実行できるコマンドを用意 - バージョン依存を避けるため - makeコマンドを用意 - コマンド共有めんどくさいのでw - modelとserverとspecで分けて生成してる - configファイル書いて分けてる 生成方法 $ go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest $ oapi-codegen petstore-expanded.yaml > petstore.gen.go

Slide 13

Slide 13 text

OpenAPIのドキュメント docs/  └ openapi/    ├ crm.yaml    └ foundation.yaml ディレクトリ構成 Goのソースコード alumnote-crm-api/  └ src/    └ openapi/      ├ model.cfg.yaml … modelの設定ファイル      ├ model.gen.go … modelの生成されたファイル      ├ server.cfg.yaml … serverの設定ファイル      ├ server.gen.go … serverの生成されたファイル      ├ spec.cfg.yaml … specの設定ファイル      └ spec.gen.go … specの生成されたファイル

Slide 14

Slide 14 text

OpenAPIのドキュメントに変更を加える   ↓ マージする   ↓ バックエンドとフロントエンドの両方でコードの生成をする Alumnoteでの開発フロー

Slide 15

Slide 15 text

生成されるレスポンスとリクエストの構造体を意識しながらrefを切った! - OpenAPIではrefを使うことで別のところに書いたモデルを参照できる - oapi-codegenではrefを切ることでGoの構造体として同じ名前で切り出される - 構造体にちゃんとした名前をつけてあげるためにrefを切った - リクエストやレスポンスで深い構造のjsonになる場合refを切っておくと書きやすくなる 工夫したこと

Slide 16

Slide 16 text

深い構造のjsonでrefを切らない場合… OpenAPIのドキュメント → 工夫したこと ListUsersResponse: type: object required: - data - total properties: data: type: array items: type: object required: - id - organization_id properties: id: type: string format: uuid organization_id: type: string format: uuid total: type: integer

Slide 17

Slide 17 text

深い構造のjsonでrefを切らない場合… 生成されたソースコード ↓ - 構造体の構造体になってしまう… - しかも配列なのでforループで回して代入したい時にやりにくい… 工夫したこと // ListUsersResponse defines model for ListUsersResponse. type ListUsersResponse struct { Data []struct { Id openapi_types.UUID `json:"id"` OrganizationId openapi_types.UUID `json:"organization_id"` } `json:"data"` Total int `json:"total"` }

Slide 18

Slide 18 text

深い構造のjsonでrefを切った場合… OpenAPIのドキュメント ↓ 工夫したこと ListUsersResponse: type: object required: - data - total properties: data: type: array items: $ref: '#/components/schemas/User' total: type: integer User: type: object required: - id - organization_id properties: id: type: string format: uuid organization_id: type: string format: uuid

Slide 19

Slide 19 text

深い構造のjsonでrefを切った場合… 生成されたソースコード ↓ - 別の構造体に生成してくれるため、使いやすい! 工夫したこと // ListUsersResponse defines model for ListUsersResponse. type ListUsersResponse struct { Data []User `json:"data"` Total int `json:"total"` } // User defines model for User. type User struct { Id openapi_types.UUID `json:"id"` OrganizationId openapi_types.UUID `json:"organization_id"` }

Slide 20

Slide 20 text

生成されるレスポンスとリクエストの構造体を意識しながらrefを切った! \そのおかげでフロントでも変な型が生成されてないっぽい!/ 工夫したこと

Slide 21

Slide 21 text

requiredをちゃんと意識してつけた! - リクエスト、レスポンス共にrequiredをつけ忘れるとポインタ付きで生成されてしまう… - 値を変数に入れるたびにポインタへ変更しなくてはいけなくてめんどくさい… 工夫したこと

Slide 22

Slide 22 text

requiredなしの場合… OpenAPIのドキュメント ↓ 工夫したこと ListUsersResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/User' total: type: integer User: type: object properties: id: type: string format: uuid organization_id: type: string format: uuid

Slide 23

Slide 23 text

requiredなしの場合… 生成されたソースコード ↓ - ポインタだらけ>< 工夫したこと // ListUsersResponse defines model for ListUsersResponse. type ListUsersResponse struct { Data *[]User `json:"data,omitempty"` Total *int `json:"total,omitempty"` } // User defines model for User. type User struct { Id *openapi_types.UUID `json:"id,omitempty"` OrganizationId *openapi_types.UUID `json:"organization_id,omitempty"` }

Slide 24

Slide 24 text

requiredありの場合… OpenAPIのドキュメント ↓ 工夫したこと ListUsersResponse: type: object required: - data - total properties: data: type: array items: $ref: '#/components/schemas/User' total: type: integer User: type: object required: - id - organization_id properties: id: type: string format: uuid organization_id: type: string format: uuid

Slide 25

Slide 25 text

requiredありの場合… 生成されたソースコード ↓ - ポインタじゃなくなる! 工夫したこと // ListUsersResponse defines model for ListUsersResponse. type ListUsersResponse struct { Data []User `json:"data"` Total int `json:"total"` } // User defines model for User. type User struct { Id openapi_types.UUID `json:"id"` OrganizationId openapi_types.UUID `json:"organization_id"` }

Slide 26

Slide 26 text

oneOfを使うとinterface{}で生成されてしまう - oneOfとはOpenAPIのスキーマを複数用意して、その中から1つを使用する場合に使う命令 - 例えばある条件の時はAという構造体を返すけど、違う条件の時はBという構造体を返 す、みたいな時など… - interface{}で生成されてしまうので、echoのBindメソッドでよしなにリクエストを構造体に マッピングできない時がある! 工夫したこと

Slide 27

Slide 27 text

OpenAPIのドキュメント ↓ 工夫したこと ProfileRowForSearchConditionModel: description: 範囲指定をする場合は dateRangeを使う。 type: object required: - profile_column_id - value properties: profile_column_id: type: string format: uuid value: oneOf: - $ref: '#/components/schemas/ProfileRowValue' - $ref: '#/components/schemas/DateRange'

Slide 28

Slide 28 text

生成されたソースコード ↓ 工夫したこと // 範囲指定をする場合は dateRangeを使う。 type ProfileRowForSearchConditionModel struct { ProfileColumnId openapi_types.UUID `json:"profile_column_id"` Value interface{} `json:"value"` } // ProfileRowValue defines model for ProfileRowValue. type ProfileRowValue interface{} // DateRange defines model for DateRange. type DateRange struct { End *openapi_types.Date `json:"end,omitempty"` Start *openapi_types.Date `json:"start,omitempty"` }

Slide 29

Slide 29 text

oneofを使うとinterface{}で生成されてしまう - echoのBindメソッドを使うとValueのinterface{}にmap[string]anyで値が入ってくるので、 そこから値を取得して本来使いたかった構造体に詰め直した - もっといい方法あったのだろうか… - フロントはいい感じに型にしてくれるみたい…ずるい… 工夫したこと

Slide 30

Slide 30 text

- CIで自動生成 - 差分のプルリクを自動で投げるなど… - 開発人数増えると生成のタイミングとフローが改修に影響するからどうするか - Aさん: OpenAPIのドキュメントを変更し、ソースコード側の変更を忘れる… - Bさん: OpenAPIのドキュメントを変更し生成したら自分が変更したところ以外も差分が 出る -> 修正範囲が大きくなる - ↑みたいになりそう、とか… 今後工夫していきたいこと

Slide 31

Slide 31 text

まとめ - OpenAPIについてとAlumnoteでの事例を紹介したよ - OpenAPIで自動生成すれば時間の節約になり少人数開発がやりやすい! - 業務委託で土日勤務の人がいてもバックエンドとフロントエンドで意思疎通しやすい!!! - 使ってよかった!!!!!!!!

Slide 32

Slide 32 text

募集中! Alumnoteでは正社員・業務委託として一緒に働いて頂ける方を募集中! 応募お待ちしてます! バックエンド https://herp.careers/v1/alumnote/PVuMatXQrdmn フロントエンド https://herp.careers/v1/alumnote/N8ujo5TyyP8h