見出し画像

コードから直感的なAPIドキュメントを作成:GinとSwagを使った効率的な手法

GINフレームワークユーザーのためのAPI文書生成ガイド

スカラーAPI文書

はじめに:

開発者として、RESTful APIを使用したWeb開発において、特にバックエンドとフロントエンドのDTOの変更に関連する問題に直面したことはありませんか?そんなあなたにこの記事は最適です。

B -> Backend Developer
F -> Frontend Developer

B changes the DTO and F keeps retrying.
After frustration 😫 
F ask for B and then 
B replies Sorry I forgot to inform 🙇‍♂️.

このような経験がある方は、この記事を最後まで楽しんでいただけるはずです。
この問題に対しては、複数の解決策があります:

  1. Bruno、PostmanなどのAPIクライアントでAPIコレクションを管理し、更新を忘れないようにする

  2. Swagger文書を手動で作成し、更新を忘れないようにする

  3. APIファーストの開発手法を採用する

  4. コードベースからAPI文書を生成する

上記の中で、3番目と4番目の選択肢が1番目と2番目よりも優れています。

コードベースからAPI文書を生成できるWebフレームワークは多数あります。例えば:
Python [FastAPI]、JavaScript [ElysiaJS]、Go [Huma.rocks]など、多数存在します。

GINにおけるAPI文書の生成

しかし、それでもGINを使用しながらコードからAPI文書を生成したい場合は、まさに適切な場所にいらっしゃいました。

コードベースからAPI文書を生成するには、いくつかの方法があります:

  • コントローラーにコメントを記述する gin-swagger

  • GINルーター用のラッパーライブラリを使用する swag

GINはデフォルトではコードベースからAPI文書を生成する機能を提供していません。また、コントローラーの上にコメントを書くことはコードの品質を低下させる可能性があります。

注意:swagパッケージは7年間更新されていません。

この記事では、swagライブラリを使用してGINのコードベースからAPI文書を生成する方法を説明します。

Gin を使用したシンプルな REST API の実装

package main

import "github.com/gin-gonic/gin"

func main() {
	r := gin.Default()
	userRepository := NewUserRepository()
	userController := NewUserController(userRepository)
	userRoute := NewUserRoute(userController)
	userRoute.RegisterUserRoutes(r)
	r.Run(":8000")
}

// ROUTE LAYER
type UserRoute struct {
	userController *UserController
}

func NewUserRoute(userController *UserController) *UserRoute {
	return &UserRoute{userController: userController}
}

func (u *UserRoute) RegisterUserRoutes(r *gin.Engine) {
	r.GET("/users", u.userController.getUsers)
	r.GET("/users/:id", u.userController.getUserByID)
	r.POST("/users", u.userController.addUser)
	r.PUT("/users", u.userController.updateUser)
	r.DELETE("/users/:id", u.userController.deleteUser)
}

// MODEL LAYER
type User struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	Age  int    `json:"age"`
}

// CONTROLLER LAYER
type UserController struct {
	router         *gin.Engine
	userRepository *UserRepository
}

func NewUserController(userRepository *UserRepository) *UserController {
	return &UserController{userRepository: userRepository}
}

func (h *UserController) getUsers(c *gin.Context) {
	users := h.userRepository.GetUsers()
	c.JSON(200, users)
}

func (h *UserController) getUserByID(c *gin.Context) {
	id := c.Param("id")
	user := h.userRepository.GetUserByID(id)
	if user == nil {
		c.JSON(404, gin.H{"error": "User not found"})
		return
	}
	c.JSON(200, user)
}

func (h *UserController) addUser(c *gin.Context) {
	var user User
	if err := c.BindJSON(&user); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}
	h.userRepository.AddUser(&user)
	c.JSON(201, user)
}

func (h *UserController) updateUser(c *gin.Context) {
	var user User
	if err := c.BindJSON(&user); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}
	h.userRepository.UpdateUser(&user)
	c.JSON(200, user)
}

func (h *UserController) deleteUser(c *gin.Context) {
	id := c.Param("id")
	h.userRepository.DeleteUser(id)
	c.JSON(204, nil)
}

// REPOSTITORY LAYER
type UserRepository struct {
	users []User
}

func NewUserRepository() *UserRepository {
	return &UserRepository{
		users: []User{
			{ID: "1", Name: "Alice", Age: 25},
			{ID: "2", Name: "Bob", Age: 30},
		},
	}
}

func (r *UserRepository) GetUsers() []User {
	return r.users
}

func (r *UserRepository) GetUserByID(id string) *User {
	for _, user := range r.users {
		if user.ID == id {
			return &user
		}
	}
	return nil
}

func (h *UserRepository) AddUser(user *User) {
	h.users = append(h.users, *user)
}

func (h *UserRepository) UpdateUser(user *User) {
	for i, u := range h.users {
		if u.ID == user.ID {
			h.users[i] = *user
			return
		}
	}
}

func (h *UserRepository) DeleteUser(id string) {
	for i, user := range h.users {
		if user.ID == id {
			h.users = append(h.users[:i], h.users[i+1:]...)
			return
		}
	}
}

上記のルーターに API ドキュメントを追加していきます。

Swag を使用した API ドキュメント作成 🤘

まず、swag パッケージをインストールします:

go get github.com/savaki/swag

RegisterUserRoute メソッドでルートを登録する代わりに、[]*swagger.Endpoint を返すように変更します。
各ルートのメタデータと、リクエスト・レスポンスのペイロードを追加します。

func (u *UserRoute) RegisterUserRoute() []*swagger.Endpoint {
 endpoints := []*swagger.Endpoint{}

 getUserByID := endpoint.New(
  http.MethodGet,
  "/users/:id",
  "Get User By ID",
  endpoint.Path("id", "string", "User ID", true),
  endpoint.Handler(u.userController.getUserByID),
  endpoint.Response(http.StatusOK, User{}, "Get User By ID Response"),
 )
 addUser := endpoint.New(
  http.MethodPost,
  "/users",
  "Add User",
  endpoint.Handler(u.userController.addUser),
  endpoint.Body(User{}, "Add User Request Payload", true),
  endpoint.Response(http.StatusCreated, gin.H{}, "Add User Response"),
 )
 ... // previous code

 endpoints = append(endpoints, getUserByID, addUser, ...)
 return endpoints
}

エンドポイントを結合するヘルパー関数も作成しましたが、現時点ではユーザーエンドポイントのみなので、大きな影響はありません。

func combine(endpoints []*swagger.Endpoint) []*swagger.Endpoint {
 return append(endpoints, endpoints...)
}

次に、gin のルートを登録し、ルーターをラップする SwagRoute 構造体を導入します。

// SWAG ROUTE
type SwagRoute struct {
 router *gin.Engine
}

func NewSwagRoute(router *gin.Engine) *SwagRoute {
 return &SwagRoute{router: router}
}

ルートを登録し、API の見やすい UI を提供する Scalar ドキュメントを追加します。

func (s *SwagRoute) RegisterRoutes(endpoints []*swagger.Endpoint) {
 api := swag.New(
  swag.Endpoints(endpoints...),
  swag.Description("THis is the test description"),
  swag.Version("1.0.0"),
  swag.Title("Test Title"),
 )
 api.Walk(func(path string, endpoint *swagger.Endpoint) {
  h := endpoint.Handler.(func(c *gin.Context))
  path = swag.ColonPath(path)

  s.router.Handle(endpoint.Method, path, h)
 })

 enableCors := true
 s.router.GET("/swagger", gin.WrapH(api.Handler(enableCors)))
 s.router.LoadHTMLGlob("templates/*.html")
 s.router.GET("/docs", func(ctx *gin.Context) {
  ctx.Header("Content-Type", "text/html")
  scheme := "http://"
  if ctx.Request.TLS != nil {
   scheme = "https://"
  }
  content := fmt.Sprintf(`
  <!DOCTYPE html>
  <html>
  <head>
   <title>Scalar API Reference</title>
   <meta charset="utf-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>
  <body>
   <!-- Need a Custom Header? Check out this example <https://codepen.io/scalarorg/pen/VwOXqam> -->
   <script
   id="api-reference"
   type="application/json"
   data-url="%s"
   ></script>
   <script src="<https://cdn.jsdelivr.net/npm/@scalar/api-reference>"></script>
  </body>
  </html>
  `, scheme+ctx.Request.Host+"/swagger")
  ctx.String(http.StatusOK, content)
 })
}go

main 関数も変更します:

func main() {
 r := gin.Default()
 ... // previous code
 allUserEndpoint := userRoute.RegisterUserRoute()
 allEnpointsForSwag := combine(allUserEndpoint)
 // swag route initiated by passing gin router
 swagRoute := NewSwagRoute(r)
 // registering all routes with pretty api docs
 swagRoute.RegisterRoutes(allEnpointsForSwag)
 r.Run(":8000")
}

最後にプログラムを実行します:

go run .

localhost:8000/docs にアクセスすると: 🎉

Scalar UI を使用した Gin の API ドキュメント

UI から直接リクエストを送信できます。コードが変更されデプロイされると、フロントエンドは最新の更新を取得します。

これにより、フロントエンドとバックエンドの非同期での作業が可能になります。

コードの完全な gist はこちら:https://gist.github.com/mukezhz/3ee41a0ba7e5689193e7c959e734c645
プロジェクトのスキャフォールドに geng を使用していて、このドキュメントを追加したい場合は、こちらの例を参照してください:https://github.com/mukezhz/gin-swagger
ありがとうございます。


この記事は、2024 年 12 月に弊社のエンジニア Mukesh Chaudhary が執筆し、日本語に翻訳したものです。
英語版はこちらをクリックしてください。
https://articles.wesionary.team/modern-api-documentation-with-swagger-in-go-gin-2aee10cb7bb9


採用情報

私たちはプロダクト共創の仕組み化に取り組んでいます。プロダクト共創をリードするプロダクト・マネージャー、そして、私たちのビジョンを市場に届ける営業メンバーを募集しています!


開発パートナーをお探しの企業様へ

弊社は、グローバル開発のメリットを活かし、高い費用対効果と品質を両立しています。経験豊富で多様性のあるチームが、課題を正しく理解し、最適なシステムと優れた体験を実現します。業務システムの開発、新規事業の開発、業務効率化やDX化に関するお困りごと、ぜひ弊社にご相談ください。