コードから直感的なAPIドキュメントを作成:GinとSwagを使った効率的な手法
GINフレームワークユーザーのための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 🙇♂️.
このような経験がある方は、この記事を最後まで楽しんでいただけるはずです。
この問題に対しては、複数の解決策があります:
Bruno、PostmanなどのAPIクライアントでAPIコレクションを管理し、更新を忘れないようにする
Swagger文書を手動で作成し、更新を忘れないようにする
APIファーストの開発手法を採用する
コードベースからAPI文書を生成する
コードベースからAPI文書を生成できるWebフレームワークは多数あります。例えば:
Python [FastAPI]、JavaScript [ElysiaJS]、Go [Huma.rocks]など、多数存在します。
GINにおけるAPI文書の生成
コードベースからAPI文書を生成するには、いくつかの方法があります:
コントローラーにコメントを記述する gin-swagger
GINルーター用のラッパーライブラリを使用する swag
GINはデフォルトではコードベースからAPI文書を生成する機能を提供していません。また、コントローラーの上にコメントを書くことはコードの品質を低下させる可能性があります。
この記事では、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 にアクセスすると: 🎉
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化に関するお困りごと、ぜひ弊社にご相談ください。