🕌

Goの新星ルーティングライブラリ「Fuego」使ってみる【ジェネリクス&自動APIドキュメント】

2025/01/13に公開

連休中日、Dev.toでたまたまジェネリクス対応かつ自動APIドキュメント生成ができるルーティングライブラリがあると見かけたので遊んでみます。

触りながらの書きなぐりとなりますがご了承ください。

https://github.com/go-fuego/fuego
https://go-fuego.github.io/fuego/docs/

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
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の解析前に呼ばれるメソッドのようで、大文字で来てしまったリクエストをボディへ渡す前に小文字にしたりできるらしい。

main.go
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に近くて書きやすそうです。

main.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
}

値の取得は概ね揃っています。
https://github.com/go-fuego/fuego/blob/main/ctx.go

サーバーオプション(ポート・CORSなど)

NewServerの引数ではオプショナルパターンで様々なWithメソッドが用意されています。
CORSもここに入ります。

https://github.com/go-fuego/fuego/blob/main/server.go

main.go
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()
}

ここまでのコード

main.go
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"),
    // ...
)

https://github.com/go-fuego/fuego/blob/main/option/option.go

ルートのグループ化

グループにもルートと同じく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年の開発ロードマップも示されており、GinEchoChiに続くGoの新たな主要ルーティングライブラリになるんじゃないかと思っています。

まだプロダクションでの使用は現実的ではないですが、この先が楽しみです。

...
go.sumの依存関係にgolang-jwt/jwtがあるのがとても気になる。。。


https://github.com/go-fuego/fuego
https://go-fuego.github.io/fuego/docs/

Discussion