安心&簡単 Goとogenで始めるAPIサーバー開発
はじめに
Webアプリケーションをバックエンドとフロントエンドに分けて開発し、REST APIを利用する際、バックエンドとフロントエンド間で型情報の不一致や更新漏れが発生することがあります。これを解決するために、私たちはOpenAPIドキュメントでAPIを定義し、それに基づいてAPIを実装し、UIを構築する開発手法を採用しています。
導入してみたところ、非常に快適に感じたため記事にしてみました。
Go言語でOpenAPIドキュメントから自動的にコードを生成できるライブラリであるogenに焦点を当て、その導入手順と活用方法について詳しく解説します。ogenを利用することで、定義と実装の乖離を防ぎ、効率的かつ信頼性の高いAPIサーバーの開発が可能になります。
今回はogenの導入から使用例をコードを交えながら紹介します。次回の記事では、OpenTelemetryと組み合わせた利用方法についても解説する予定です。
コードはこちらにまとめて公開してあります。
ogenの導入
ogenのインストール
goのプロジェクトがある前提で以下のコマンドで導入します。
go get -d github.com/ogen-go/ogen
OpenAPIドキュメントからのコード生成
ここでは以下の openapi.yaml
を使用していきます。
上記 openapi.yaml
を配置し、その横にogen.go
を配置し、go generate
でapi関連のファイルを生成してもらう準備をします。
package sample_ogen_otel
//go:generate go run github.com/ogen-go/ogen/cmd/ogen@latest --target logo --package logo --clean openapi.yaml
ファイルの準備ができたらルートディレクトリでgo generate
を実行してみます。
go generate ./...
コマンドを実行すると、//go:generate
のコメントを探してそのコマンドを実行してくれます。
コマンドの引数は以下のようなものです。
-
--target
は生成されたファイルを格納するディレクトリの指定 -
--package
は生成するコードのパッケージ名の指定 -
--clean
はコード生成するときに毎回一度コードを削除して、生成を行うように指定
以下のようなファイルが生成されるかと思います
├── logo
│ ├── oas_cfg_gen.go
│ ├── oas_client_gen.go
│ ├── oas_handlers_gen.go
│ ├── oas_json_gen.go
│ ├── oas_labeler_gen.go
│ ├── oas_middleware_gen.go
│ ├── oas_parameters_gen.go
│ ├── oas_request_decoders_gen.go
│ ├── oas_request_encoders_gen.go
│ ├── oas_response_decoders_gen.go
│ ├── oas_response_encoders_gen.go
│ ├── oas_router_gen.go
│ ├── oas_schemas_gen.go
│ ├── oas_server_gen.go
│ ├── oas_unimplemented_gen.go
│ └── oas_validators_gen.go
handlerの実装
こんな感じでhandlerが定義されるのでinterfaceを実装しましょう
実装したハンドラー
リクエストボディやクエリパラメータが既に構造体として定義されているため、毎回のパース処理を実装する必要がないのは非常に助かります。
また、レスポンスも既に定義されているGoの構造体を返すだけで済みます。
serverのlisten
main関数などでサーバーを起動するようにしましょう。
生成されたコードにNewServer
という関数があるのでそれを使用します。
http.Handler
を実装する形になっていますので、既存のrouterに入れるもよし、そのままhttp.ListenAndServe
でサーバー起動するもよしです。
今回はswagger-uiでの確認もできるようにしたかったので以下のように/api
のハンドラーとして扱っています。
このようにハンドラーを引数に渡すだけでサーバーを作成できるのは便利ですね。
func newAPIRouter() (http.Handler, error) {
service := NewLogoService()
return logo.NewServer(service)
}
動作確認
mainを実行し、curlを投げて動作確認しましょう! swagger-uiでopenapi.yamlを表示してそこから操作するのも良さそうです。
1件 createした後にgetした結果は以下になります。定義通りにレスポンスが返ってきていることが確認できます。
curl -X 'GET' \
'http://localhost:8080/api/logos' \
-H 'accept: application/json' | jq
{
"logos": [
{
"logoId": "319e92a4-93f7-4c1c-a1b1-5a68f30009dc",
"name": "LOGOLABO",
"createdAt": "2024-09-29T18:39:27+09:00",
"updatedAt": "2024-09-29T18:39:27+09:00"
}
],
"totalCount": 1
}
ここまではogenでのAPIサーバーの簡単な実装でした!
エラーハンドリング
エラーの時のレスポンスはどうすればいいのでしょうか?
しれっと実装していましたが、詳しく解説していきます。
openapi.yaml
で各APIのレスポンスを書くときにdefaultを定義し、そこでエラー時の挙動を定義していました。
responses:
'200':
~~~
default:
description: General error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
ogenでコード生成を行うとhandlerの中にNewError(ctx context.Context, err error) *logo.ErrorStatusCode
というメソッドが定義されていたかと思います。
他のAPIの処理ではerrorを返すだけになっていますので、エラー時はerrorを返すだけです。error != nil
の時にこのNewError
メソッドが呼ばれる形になります。
ここでは手抜き実装ですが、カスタムエラーを定義してあげて404や、401などを返せるようにしてあげれば良さそうです。
func (l *LogoService) NewError(ctx context.Context, err error) *logo.ErrorStatusCode {
slog.ErrorContext(ctx, "detect api error", "err", err.Error())
return &logo.ErrorStatusCode{
StatusCode: 500, // TODO: error code
Response: logo.Error{
Code: 500,
Message: err.Error(),
},
}
}
認証
APIに認証はつけておきたいですよね!認証の実装を追加していきましょう。
JWTのトークンを使った簡単な認証を実装していきます。
コードの修正はこのPRが参考になります。
認証の定義を追加
openapi.yaml
に認証の定義を追加していきましょう。
まずは認証の種類の定義です。今回はbearerを使ったtoken認証なので以下のように定義します。
components:
~~~
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
それぞれのAPIに認証を追加していきます。
post:
tags:
- logo
summary: Create a new logo
operationId: createLogo
~~~
+ security:
+ - bearerAuth: [ ]
コード生成
再度 go generate ./...
を実行して生成されたコードを見てみましょう。
handlerを生成する時に作っていたNewServer
関数に変更が加えられているのが確認できるかとお思います。
// NewServer creates new Server.
-func NewServer(h Handler, opts ...ServerOption) (*Server, error) {
+func NewServer(h Handler, sec SecurityHandler, opts ...ServerOption) (*Server, error) {
s, err := newServerConfig(opts...).baseServer()
if err != nil {
return nil, err
}
SecurityHandler
といういかにもなHandlerを引数に持つようになりました。
SecurityHandler
は以下のようなinterfaceでHandleBearerAuth
メソッドを実装してあげればいいです。
// SecurityHandler is handler for security parameters.
type SecurityHandler interface {
// HandleBearerAuth handles bearerAuth security.
HandleBearerAuth(ctx context.Context, operationName string, t BearerAuth) (context.Context, error)
}
認証の実装
JWT(JSON Web Token)の扱いは省略しますが、上記のインターフェースを実装するだけです。
BearerAuth
という構造体の引数があるのでそこからtokenを受け取り、jwtの確認をしてあげればOKです。実際にはここでJWTの中からクレームを取り出して、ctx
にセットするなどの処理が追加されることでしょう。
ついでにtokenを発行するAPIも作成しておきます。
paths:
/token:
post:
tags:
- auth
summary: Get a token
operationId: getToken
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TokenRequest'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
default:
description: General error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
TokenRequest:
type: object
required:
- email
properties:
email:
type: string
description: The email of the user
TokenResponse:
type: object
required:
- token
properties:
token:
type: string
description: The token
LogoSearchResult:
type: object
required:
- logos
- totalCount
properties:
logos:
type: array
items:
$ref: '#/components/schemas/LogoDetail'
description: The logos
totalCount:
type: integer
description: The total count of logos
動作確認
まずは、tokenなしでリクエストを投げてみましょう。エラー処理が簡略化されているため500エラーになってしまいますが、ここでは無視してください。
curl -X 'GET' \
'http://localhost:8080/api/logos' \
-H 'accept: application/json' | jq
{
"code": 500,
"message": "operation GetLogoList: security \"\": security requirement is not satisfied"
}
ちゃんとエラーになりましたね!
では、トークンを発行し、それを付与してリクエストを投げてみましょう。
curl -X 'GET' \
'http://localhost:8080/api/logos' \
-H 'accept: application/json' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InIubWl5YXNoaXRhQGV4YW1wbGUuY29tIiwiZXhwIjoxNzI3NzA5NTQxLCJpc3MiOiJsb2dvIn0.BNCDDOs0plaSVTM0xIB-4PE1kfAlZ_fNVTvqWdNusa0' | jq
{
"logos": [
{
"logoId": "795ee99c-7a66-4d68-bfdb-a72a81ac9fc0",
"name": "token test",
"createdAt": "2024-09-30T00:34:57+09:00",
"updatedAt": "2024-09-30T00:34:57+09:00"
}
],
"totalCount": 1
}
トークンを付与すると、正しく値を取得することができました!
まとめ
ここまで、ogenを利用してOpenAPIベースでのAPIサーバー開発の導入を行ってみました。意外と簡単に実装できたのではないでしょうか。フロントエンド担当者と「型が定義と違う!」といったトラブルがなくなるのは非常に良い点です。
開発するコードが減るため、実装を簡略化できる点でも有効だと感じます。
他にもさまざまな機能があるので、ぜひ確認してみてください〜〜
今後、OpenTelemetryを導入してトレース情報を取得できるようにしたり、フロントエンド側での対応についても記事にしていく予定です!
Discussion