OpenAPI(APIドキュメント)を用いたGoのAPIサーバー開発
RGAでインフラエンジニアをしている川合です。
以前SHIFT EVOLVEでOpenAPIについて発表したのですが、この時はライトニングトークだったということで持ち時間も少なく、あまり詳しくは話すことができなかったので、以下の2点についてここで少し詳しく書いておこうと思います。
OpenAPIというものはなにかの概要解説
OpenAPIを用いた開発手法について簡単な説明
※元のスライドはこちらをご参照ください。
https://speakerdeck.com/shift_evolve/apihuasutofalsekai-fa
※コードはこちらにありますので合わせご参照ください。
https://github.com/kwryoh/oapi-sample
OpenAPI とは
OpenAPIはRESTful API記述方法のひとつです。
APIの各パスやリクエスト・レスポンスの形式の仕様をYAML/JSONで記述する仕様です。
現在 v3.1 まででています。(が、いまのところ各ツールの対応的に v3.0 を使う方が良いでしょう。)
https://www.openapis.org/
REST APIの記述仕様は他にもありますが、OpenAPIのメリットとして、様々な言語用のコード生成ツールやAPIゲートウェイの対応が豊富なため下の画像のようなドキュメントも簡単に作成できたりします。
openapi-generator: https://github.com/OpenAPITools/openapi-generator
APIゲートウェイ: https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-import-api.html
これらを用いることで仕様と一貫したアプリ開発ができるようになります。
また、OpenAPIドキュメントを中心とした開発とすることでスキーマ駆動開発が可能となります。
スキーマ駆動開発とは
APIドキュメントを初めに作成することで次のメリットを享受する開発手法です。
バックエンドを待たずにフロントエンド側の開発に着手できる(モックサーバーを使うため)
リクエスト、レスポンスの形式の誤りをコード上でチェックしやすい
OpenAPIに準じた設計書の作成方法
作成方法としては2つほどあります。
YAML or JSONを手作業で書く
Stoplight Studioを使う
今から行う場合は(2)の方法をおすすめします。YAML力が不要で必要な要素の記述やバリデーションなども行ってもらえるため初めて触れる場合はこちらがよいでしょう。また mock サーバーを立ち上げる事ができます。
(1)の方法の場合は、Swagger Editorを使うと正しい書き方となっているかチェックしながら記述できます。
なお設計方針がない場合は次のサイトの設計方針を参考にしてみるとよいでしょう。
https://future-architect.github.io/articles/20200409/
Stoplight Studio
Stoplight Studio: https://stoplight.io/studio/
Swagger Editor
Swagger Editor Live Demo: https://editor.swagger.io/
サンプル作成
実際にOpenAPIを使った開発を行ってどんな感じになるのか試してみます。
次のツールでAPIサーバーを作ります。
構成
OpenAPI作成ツール
Stoplight Studio
Go
chi
sqlc
コードジェネレータ
oapi-codegen
sqlc (DB用)
Database
PostgreSQL 13
APIドキュメントの作成
まずはAPIドキュメントを作成するため、Stoplight Studioを立ち上げます。
今回は確認用に作るだけなので「New Local Poroject」にプロジェクト名を入力します。
Githubでドキュメントを管理する場合は「Open Git Project」にレポジトリURLを入力することで可能です。
その後「API」を選び名前を入力します。また、「Show Advanced」よりバージョンを「3.0」に変更しておきます。
入力すると次の画面が表示されます。
基本的には
"Paths"にAPIを追加する
追加したAPIで各メソッドごとのリクエスト、レスポンスを設定する
リクエストやレスポンスでJSONオブジェクト使う場合は"Models"で定義し、それを使用する
パラメータが必要な場合は"Parameters"に追加し使用する
の繰り返し
※元々あるパスやモデルはサンプルなので削除して問題ありません。
各機能については次のとおりです。
API Overview
APIについての説明やサーバーへのURL、セキュリティ情報などを設定できる画面です。
各APIに設定するタグもここで作成します。
Paths
右クリック「New Path」で新しいパスを作成できます。任意のパスを入力し確定します。
パスパラメータを入力するときは"{} 鉤括弧"でくくる。(先に"Parameters"の方でパラメータを作成しておくとよい)
作成したパスを選択すると各メソッド毎の仕様作成画面が表示される。
Summary: APIの簡易的な説明(メソッド毎に別)
Path Params: パラメータがある場合、パラメータのデータ型、概要などを定義できる
Operation ID: Goやその他の言語でのハンドラ名、ファンクション名となる
Description: APIの概要(メソッド毎に別)
Add Body: リクエストでリクエストボディを送る場合、そのデータ型などを定義する(登録、更新する場合など)
Response: APIが返すレスポンスについてステータスコードとデータを定義する
Models
リソースモデルを定義する場合、ここで右クリックをし「New Model」とする。
モデル名を入力し編集画面を表示する。
図の通りポチポチとプロパティや値を定義していく。型はJSONに準拠
Request Bodies
各APIで使用するリクエストボディの定義をこの部分で行う。
記述方法は前述の"Models"と同じです。こちらで定義したものを"Paths"のリクエストボディで参照することができます。
Responses
各APIから返却されるレスポンスを定義します。
こちらも記述方法は前述の"Models"と同様です。こちらで定義したものを"Paths"のレスポンス設定で参照することができます。
Parameters
パスパラメータ、クエリパラメータの定義を行う画面です。
こちらで定義したものを"Paths"のPath paramで参照することができます。
これだけ参照がForm上からできないので、既存のモデルを参照したい場合はCodeで直接$ref設定を記載する必要があります。
備考
右上にOASのLint結果がアイコンで表示されています。エラーが残っているとうまく使用できないのでエラー、ワーニングは取り除くようにしましょう。
ファイル保存は自動で行われています。Githubと連携している場合はGUI上でコミットすることが可能です。
モデル名の命名規則などこちらが参考になると思います。https://future-architect.github.io/articles/20200409/
oapi-codegenでGoコードの生成
OpenAPIのYAMLからGoコードを生成します。今回は oapi-codegen を使用しました。
まず oapi-codegen を導入します。 https://github.com/deepmap/oapi-codegen
go get github.com/deepmap/oapi-codegen/cmd/oapi-codegen
次にGoのコードを生成します。
oapi-codegen docs/openapi.yaml -package openapi -generate chi-server,spec -o gen/openapi/server.gen.go
oapi-codegen docs/openapi.yaml -package openapi -generate types -o gen/openapi/types.gen.go
このコードを使ってAPIサーバーを作っていきます。
なお生成したコードは直接編集せず別途利用するためのコードを作成していきます。
アプリケーションコードの追加
前述で生成したコードはインターフェースやリクエスト・レスポンスの入出力周りだけなのでリクエストを受け取った際どういう処理をするかは手で書かなければいけません。
生成されたコードは次のような形になっています。
server.gen.go (抜粋)
// Package openapi provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen version v1.8.2 DO NOT EDIT.
package openapi
import (
"bytes"
"compress/gzip"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"github.com/deepmap/oapi-codegen/pkg/runtime"
"github.com/getkin/kin-openapi/openapi3"
"github.com/go-chi/chi/v5"
)
// ServerInterface represents all server handlers.
type ServerInterface interface {
// Get item list
// (GET /items)
GetItems(w http.ResponseWriter, r *http.Request, params GetItemsParams)
// Create Item
// (POST /items)
PostItems(w http.ResponseWriter, r *http.Request)
// Delete item
// (DELETE /items/{item_id})
DeleteItem(w http.ResponseWriter, r *http.Request, itemId ItemId)
// Get Item
// (GET /items/{item_id})
GetItemById(w http.ResponseWriter, r *http.Request, itemId ItemId)
// Update item
// (PATCH /items/{item_id})
PatchItem(w http.ResponseWriter, r *http.Request, itemId ItemId)
}
// ServerInterfaceWrapper converts contexts to parameters.
type ServerInterfaceWrapper struct {
Handler ServerInterface
HandlerMiddlewares []MiddlewareFunc
}
type MiddlewareFunc func(http.HandlerFunc) http.HandlerFunc
ServerInterface という型に各ハンドラのインターフェースが定義されており、これらのインターフェースを実装することでAPIサーバーが構築できます。
では実装していきましょう。
まずファイルを作成し適当に型を作成します。また ServerInterface 型を漏れなく実装できていることを確かめるコードを追加しておきましょう。
api.go
type ApiServer struct {}
var _ ServerInterface = (*ApiServer)(nil)
次にこの型の生成を楽にするNew関数と各ハンドラ実装を作成していきましょう。
各種型は同じく生成した types.gen.go で確認できます。
(chiで実装していますが、echoでも同じ様にできます。)
// New関数
func NewApiServer() *ApiServer {
return &ApiServer{}
}
// 任意の集合を返すハンドラ実装
func (i *ItemStore) GetItems(w http.ResponseWriter, r *http.Request, params GetItemsParams) {
// /items?limit=30&page=2 などのクエリパラメータを取得
var limit := 10
if params.Limit != nil {
limit = params.Limit
}
// データ取得処理など
// レスポンスデータの作成
var resp GetItemsResponse
/* 諸々の処理 */
w.WriteHeader(http.StatusOK)
render.JSON(w, r, resp)
}
// 特定のItemデータを返すハンドラ実装
func (i *ItemStore) GetItemById(w http.ResponseWriter, r *http.Request, itemId ItemId) {
// /items/{item_id} パスパラメータを取得する(例:/items/1 の場合は 1 が返る)
var id = itemId
// データ取得処理など
// レスポンスデータの作成
var resp GetItemsResponse
/* 諸々の処理 */
w.WriteHeader(http.StatusOK)
render.JSON(w, r, resp)
}
// ... ハンドラ分実装する
最後にこのハンドラをルーターに読み込ませていきます。
main.go などで次のようにルーター(サーバー)を実装します。
(エラー処理など除いて簡易実装としています)
main.go
package main
import (
"fmt"
"log"
"net/http"
"github.com/go-chi/chi/v5"
middleware "github.com/deepmap/oapi-codegen/pkg/chi-middleware"
api "path/to/your_repo/gen/openapi"
)
func main() {
// OpenAPI定義管理型の生成
swagger, _ := api.GetSwagger()
// Swaggerドキュメントを公開しないように設定
swagger.Servers = nil
// ハンドラを実装した型を生成
apiServer := api.NewApiServer()
// chi のルーターを生成
r := chi.NewRouter()
// HTTPリクエストがAPI仕様に沿っているかのチェック機構を実装
r.Use(middleware.OapiRequestValidator(swagger))
// ハンドラをルーターに実装
api.HandlerFromMux(apiServer, r)
// APIサーバーを起動
http.ListenAndServe(":9000", r)
}
ここまでできたら、go run とすればAPIサーバーが立ち上がり、curlなどで定義したパスにリクエストを送ればレスポンスが返ってくるはずです。
APIの仕様に沿った動作となっているか確認しながら実装を進めていきます。
コードの生成について
Goにはgenerateサブコマンドがあり、コード生成のコマンドをコメントで記載しておけばgo generateと打つだけでコードが生成されます。
ハンドラ実装を記載したコードの文頭に次のようなコメントを記載します。
package openapi
//go:generate go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.8.2
//go:generate oapi-codegen docs/openapi.yaml -package openapi -generate chi-server,spec -o gen/openapi/server.gen.go
//go:generate oapi-codegen docs/openapi.yaml -package openapi -generate types -o gen/openapi/types.gen.go
コードの生成は次のコマンドを実行します。
go generate
すると個別のコマンドを打たなくても記述に沿ったコマンドが実行されます。
さいごに
今回、APIドキュメントとコードを同じ人で作成しましたが、APIドキュメントの作成とコードの作成を分担し(もしくはレポジトリを分ける)場合、APIドキュメントの修正にコードの修正も追随しやすくなります。
またフロントエンド側との結合もミスが少なくなるでしょう。
なお、今回使用した oapi-codegen はレスポンスの型定義がされていないため、レスポンスのデータ型は開発者に委ねられる形となってしまいます。
極力レスポンスのデータ型はこの型を使うなどルールを共有しておき
仕様書との齟齬が無いように気をつけましょう。
備考
他のREST API記述仕様としてRAMLやBlueprintなどがあります。
RAML: https://raml.org/
Blueprint: https://apiblueprint.org/
そのほかAPI定義を元にコード生成する機構が同様にあるgRPCもあります。
gRPC: https://grpc.io/
__________________________________
【ご案内】
ITシステム開発やITインフラ運用の効率化、高速化、品質向上、その他、情シス部門の働き方改革など、IT自動化導入がもたらすメリットは様々ございます。
IT業務の自動化にご興味・ご関心ございましたら、まずは一度、IT自動化の専門家リアルグローブ・オートメーティッド(RGA)にご相談ください!
お問合せは以下の窓口までお願いいたします。
【お問い合わせ窓口】
代表窓口:info@rg-automated.jp
URL: https://rg-automated.jp