Goの新星ルーティングライブラリ「Fuego」使ってみる【ジェネリクス&自動APIドキュメント】
連休中日、Dev.toでたまたまジェネリクス対応かつ自動APIドキュメント生成ができるルーティングライブラリがあると見かけたので遊んでみます。
触りながらの書きなぐりとなりますがご了承ください。
Why Fuego?
Chi、Gin、Fiber、Echo は優れたフレームワークです。ただし、これらはかなり前に設計されたため、 現在の API ではシグネチャから OpenAPI 型を推測できません。これはジェネリックで可能になりました。Fuego は、API や Web アプリケーションの開発を容易にする「最新の Go ベース」の機能を多数提供しています。
- OpenAPI3互換のドキュメントを自動作成(Yamlやコメントを書く必要なし)
-
net/http
パッケージと100%の互換性を担保(Go1.22基準) - リクエストの自動デコード・エンコード(JSON、XML、HTML)
- リクエストのバリデーション
- リクエスト時点で指定のデータ変換(大文字→小文字など?)
など、目立つところで上記の特徴があります。
最近はHumaという似た特徴をもつライブラリも出ましたが、Fuegoはより直感的でecho
に近い印象を受けます。
開発もゴリゴリで最終コミットが4日前となってますね。
案の定日本語の情報は皆無ですが、薄いパッケージなので概ね問題ないはず。。
Hello World!
❯ mkdir fuego-test && cd fuego-test && go mod init fuego-test
❯ go get github.com/go-fuego/fuego
❯ touch main.go
❯ ls
go.mod go.sum main.go
package main
import "github.com/go-fuego/fuego"
func main() {
s := fuego.NewServer() // デフォルト:9999
fuego.Get(s, "/", func(c fuego.ContextNoBody) (string, error) {
return "Hello, World!", nil
})
s.Run()
}
これで一旦立ち上げてみると、
❯ go run .
2025/01/13 02:09:45 INFO Server running ✅ on http://localhost:9999 "started in"=78.224µs
2025/01/13 02:09:45 INFO JSON spec: http://localhost:9999/swagger/openapi.json
2025/01/13 02:09:45 INFO OpenAPI UI: http://localhost:9999/swagger/index.html
2025/01/13 02:09:45 INFO JSON file: doc/openapi.json
これだけでサーバーと美しいAPIドキュメントが生成され、ブラウザで確認できます。
http://localhost:9999/swagger/index.html
リクエスト・レスポンスのスキーマ・バリデーション
構造体でスキーマを定義します。
小文字のフィールドは無視され、大文字のフィールドはそのままボディになります。
バリデーションにはgo-playground/validatorを使用しているようなので、構造体に定義します。
InTransform
はBodyの解析前に呼ばれるメソッドのようで、大文字で来てしまったリクエストをボディへ渡す前に小文字にしたりできるらしい。
type PersonRequest struct {
Name string `json:"name" validate:"required"`
Age int `json:"age"`
}
// InTransformはBodyの解析前に呼び出される
func (r *PersonRequest) InTransform(context.Context) error {
r.Name = strings.ToLower(r.Name) // 小文字にしたり
return nil
}
// おまじない
var _ fuego.InTransformer = (*PersonRequest)(nil)
type PersonResponse struct {
Message string `json:"message"`
}
ハンドラー関数
ハンドラー関数のシグネチャはこんな感じです。
リクエストボディない→func(c fuego.ContextNoBody) (T, error)
リクエストボディある→func(c fuego.ContextWithBody[T]) (T, error)
Connect-go
に近くて書きやすそうです。
// リクエストにボディがなければ ContextNoBody
// リクエストにボディがあれば ContextWithBody[T]
func helloPerson(c fuego.ContextWithBody[PersonRequest]) (PersonResponse, error) {
body, err := c.Body()
if err != nil {
return PersonResponse{}, err
}
return PersonResponse{
Message: "Hello, " + body.Name,
}, nil
}
値の取得は概ね揃っています。
サーバーオプション(ポート・CORSなど)
NewServer
の引数ではオプショナルパターンで様々なWith
メソッドが用意されています。
CORSもここに入ります。
func main() {
s := fuego.NewServer(
// 任意のポートへ変更
fuego.WithAddr("localhost:8080"),
// cors設定
fuego.WithCorsMiddleware(cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
}).Handler),
)
fuego.Post(s, "/hello", helloPerson)
s.Run()
}
ここまでのコード
package main
import (
"context"
"strings"
"github.com/go-fuego/fuego"
"github.com/rs/cors"
)
type PersonRequest struct {
Name string `json:"name" validate:"required"`
Age int `json:"age"`
}
type PersonResponse struct {
Message string `json:"message"`
}
// InTransformはBodyの解析前に呼び出される
func (r *PersonRequest) InTransform(context.Context) error {
r.Name = strings.ToLower(r.Name) // 小文字にしたり
return nil
}
var _ fuego.InTransformer = (*PersonRequest)(nil) // おまじない
// リクエストにボディがあれば ContextWithBody[T]
// リクエストにボディがなければ ContextNoBody
func helloPerson(c fuego.ContextWithBody[PersonRequest]) (PersonResponse, error) {
body, err := c.Body()
if err != nil {
return PersonResponse{}, err
}
return PersonResponse{
Message: "Hello, " + body.Name,
}, nil
}
func main() {
s := fuego.NewServer(
// 任意のポートへ変更
fuego.WithAddr("localhost:8080"),
// cors設定
fuego.WithCorsMiddleware(cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
}).Handler),
)
fuego.Post(s, "/hello", helloPerson)
s.Run()
}
リクエストしてみる
APIドキュメントのリクエストサンプルをコピペして、試しにname
を大文字でリクエストすると、ちゃんと小文字に変換されています。
❯ curl --request POST \
--url http://localhost:9999/hello \
--header 'Accept: application/json, application/xml' \
--header 'Content-Type: */*' \
--data '{
"age": 0,
"name": "Age"
}'
{"message":"Hello, age"}
name
は必須項目なので空でリクエストするとこちらもしっかりエラーを返してくれました。
{
"title": "Validation Error",
"status": 400,
"detail": "Name is required",
"errors": [
{
"name": "PersonRequest.Name",
"reason": "Key: 'PersonRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag",
"more": {
"field": "Name",
"nsField": "PersonRequest.Name",
"param": "",
"tag": "required",
"value": ""
}
}
]
}
APIドキュメントの説明追加
ドキュメントの追加は各ルート、グループごとに設定可能のようです。
github.com/go-fuego/fuego/option
パッケージに設定可能なオプションがまとまっています。
fuego.Post(s, "/hello", helloPerson,
option.Summary("helloPerson"),
option.Description("Person api the description sample"),
option.Tags("Person"),
// ...
)
ルートのグループ化
グループにもルートと同じくAPIドキュメントの仕様を記載できます。
// ...
userRoute := fuego.Group(s, "/users",
option.Summary("Users routes"),
option.Description("Default description for all Users routes"),
option.Tags("users"),
)
// GET /users/hello
// グループ内のエンドポイントは第一引数がグループを格納した変数になります。
// ↓
fuego.Get(userRoute, "/hello", helloWorld,
option.Summary("A simple hello world"),
)
ミドルウェア
今のところ、Fuegoから独自のミドルウェアは提供していないそうですが、net/http
の完全互換なので、func(next http.Handler) http.Handler
であればなんでも登録可能なので、chi
が提供するミドルウェア等も使用することができます。
// ...
s := fuego.NewServer()
fuego.Use(s, func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// ...
next.ServeHTTP(w, r)
})
})
雑まとめ
自分でも触ってみながら、かなりざっくりとした紹介になりすみません。
まだ大部分を把握できていない状態でも個人的にはecho
の現代版な印象を受け最高のライブラリだと感じました。
2025年の開発ロードマップも示されており、Gin
・Echo
・Chi
に続くGoの新たな主要ルーティングライブラリになるんじゃないかと思っています。
まだプロダクションでの使用は現実的ではないですが、この先が楽しみです。
...
go.sum
の依存関係にgolang-jwt/jwt
があるのがとても気になる。。。
Discussion