Published on

API Service with Go: API Documents

Authors

API Documents

สิ่งสำคัญอีกอย่างเมื่อเราสร้ง API Service คือ API Document ว่า API ของเราทำงานยังไง มีการรับ Resquest แบบ มี Response หน้าตาเป็นยังไง

ซึ่งที่นิยมใช้กันคือ การสร้าง API Document ด้วย Swagger ซึ่งในภาษา Go จะใช้ https://github.com/swaggo/swag ในการทำ ซึ่งรองรับหลาย web framework

ในบทความนี้จะใช้ fiber ต้องใช้ package fiber-swagger ซึ่งจะเป็น middleware ที่สร้าง API document ด้วย Swagger 2.0 แบบอัตโนมัติ

การสร้าง Swagger 2.0 Api documents

เราจะใช้โค้ดต่อจากบทความที่แล้ว มาสร้าง Swagger 2.0 Api documents

  1. เริ่มจากติดตั้ง Swag

    go install github.com/swaggo/swag/cmd/swag@latest
    
  2. รัน Swag เพื่อสร้างไฟล์ที่จำเป็น (pkg/docsfolder และ pkg/docs/doc.go)

    swag init --parseDependency -g pkg/module/module.go -o pkg/docs
    
  3. แก้ไข pkg/module/module.go เพื่อใช้ middleware ในการ generate doc ui ขึ้นมา

    pkg/module/module.go
    package module
    
    import (
    	// ...
    	"goapi-doc/pkg/docs"
    	fiberSwagger "github.com/swaggo/fiber-swagger"
    )
    
    func Init(ctx *app.Context) {
    	todo.Init(ctx)
    
    	ctx.Router.Get("/healthz", healthCheckHandler)
    
    	//Swagger Doc details
    	host := ctx.Config.Gateway.Host
    	basePath := ctx.Config.Gateway.BaseURL
    
    	if len(host) == 0 {
    		host = fmt.Sprintf("localhost:%v", ctx.Config.Server.Port)
    	}
    
    	if len(basePath) == 0 {
    		basePath = ctx.Config.App.BaseUrl
    	}
      // แก้ไขรายละเอียด api doc
    	docs.SwaggerInfo.Title = "Todo Service API Document"
    	docs.SwaggerInfo.Description = "List of APIs for Todo Service."
    	docs.SwaggerInfo.Version = "1.0"
    	docs.SwaggerInfo.Host = host
    	docs.SwaggerInfo.BasePath = basePath
    	docs.SwaggerInfo.Schemes = []string{"https", "http"}
    
    	//Init Swagger routes
    	ctx.Router.Get("/swagger/*", fiberSwagger.WrapHandler)
    }
    
    func healthCheckHandler(c *fiber.Ctx) error {
    	return c.SendStatus(http.StatusOK)
    }
    
  4. ทดสอบรัน และเปิด browser ไปที่ http://localhost:8080/swagger/index.html ก็จะได้ Swagger 2.0 Api documents

Doc

ใส่รายละเอียด

เพิ่มรายละเอียดให้แต่ละ endpoints รับ request และตอบกลับ response ยังไง จะใช้วิธีการใส่ comments

Comments ใส่อะไรได้บ้าง

  • @Summary → ใช้บอกว่า api เส้นนี้ทำอะไร
  • @Description → ใช้ใส่รายละเอียดแบบยาว
  • @Tags → ใช้กำหนด tag ใส่ได้หลาย tag แยกด้วย commas
  • @Accept → ใช้กำหนดรูปแบบ request เช่น json
  • @Produce → ใช้กำหนดรูปแบบ response เช่น json
  • @Param → ใช้กำหนดข้อมูลที่ส่งมา เช่น body ใช้ struct ตัวไหน
    • รูปแบบ [param name] [param type] [data type] [is mandatory?] [comment] [attribute(optional)]
    • ตัวอย่าง term query string false "filter the text based value (ex: term=dosomething)"
  • @Failure → ใช้กำหนด error reponse ทั้ง code และ body ใช้ struct ตัวไหน
    • รูปแบบ [return code or default] [{param type}] [data type] [comment]
    • ตัวอย่าง @Failure 422 {object} swagdto.Error422
  • @Success → ใช้กำหนด success reponse ทั้ง code และ body ใช้ struct ตัวไหน
    • รูปแบบ [return code or default] [{param type}] [data type] [comment]
    • ตัวอย่าง @Success 200 {object} swagdto.ResponseWithPage{data=swagger.TodoSampleListData}
  • @Router → ใช้กำหนด path และ method
    • รูปแบบ path [httpMethod]
    • ตัวอย่าง @Router /todos [get]

ดูเพิ่มเติมได้ที่ https://github.com/swaggo/swag#api-operation

Request & Response Object

ในกรณีที่มีการรับ request เป็น json เราจะต้องเอา dto struct มารับค่า และการตอบกลับ response เราก็จะเอา dto struct มาแปลงเป็น json ตอบกลับไป ส่วนจะไม่เอา dto struct มาใช้ แต่ให้สร้าง struct ใหม่สำหรับ documents ขึ้นมาแทน และมีการใส่ตัวอย่างข้อมูลโดยใช้ example tag

  • Error struct เอาไว้เป็นตัวอย่างข้อมูล response กรณีระบบทำงานผิดผลาด ซึ่งโครงสร้าง json จะเหมือนกัน เลยจะสร้างเอาไว้ใน pkg/common/swagdto/error.go

    pkg/common/swagdto/error.go
    package swagdto
    
    type ErrorDetail struct {
    	Target  string `json:"target" example:"name"`
    	Message string `json:"message" example:"name field is required"`
    }
    
    type ErrorData400 struct {
    	Code    string `json:"code" example:"400"`
    	Message string `json:"message" example:"Bad Request"`
    }
    
    // ตัวอย่าง error response กรณีส่ง request data มาผิดรูปแบบ
    type Error400 struct {
    	Status    uint         `json:"status" example:"400"`
    	Error     ErrorData400 `json:"error"`
    	RequestId string       `json:"requestId" example:"3b6272b9-1ef1-45e0"`
    }
    
    type ErrorData401 struct {
    	Code    string `json:"code" example:"401"`
    	Message string `json:"message" example:"Unauthorized"`
    }
    
    // ตัวอย่าง error response กรณีไม่ได้ login
    type Error401 struct {
    	Status    uint         `json:"status" example:"401"`
    	Error     ErrorData401 `json:"error"`
    	RequestId string       `json:"requestId" example:"3b6272b9-1ef1-45e0"`
    }
    
    type ErrorData403 struct {
    	Code    string `json:"code" example:"403"`
    	Message string `json:"message" example:"Forbidden"`
    }
    
    // ตัวอย่าง error response กรณีไม่มีสิทธิการใช้งาน
    type Error403 struct {
    	Status    uint         `json:"status" example:"403"`
    	Error     ErrorData403 `json:"error"`
    	RequestId string       `json:"requestId" example:"3b6272b9-1ef1-45e0"`
    }
    
    type ErrorData404 struct {
    	Code    string `json:"code" example:"404"`
    	Message string `json:"message" example:"Not Found"`
    }
    
    // ตัวอย่าง error response กรณีค้นหารายการไม่เจอ
    type Error404 struct {
    	Status    uint         `json:"status" example:"404"`
    	Error     ErrorData404 `json:"error"`
    	RequestId string       `json:"requestId" example:"3b6272b9-1ef1-45e0"`
    }
    
    type ErrorData422 struct {
    	Code    string        `json:"code" example:"422"`
    	Message string        `json:"message" example:"invalid data see details"`
    	Details []ErrorDetail `json:"details"`
    }
    
    // ตัวอย่าง error response กรณีตรวจสอบข้อมูล struct ไม่ผ่าน
    type Error422 struct {
    	Status    uint         `json:"status" example:"422"`
    	Error     ErrorData422 `json:"error"`
    	RequestId string       `json:"requestId" example:"3b6272b9-1ef1-45e0"`
    }
    
    type ErrorData500 struct {
    	Code    string `json:"code" example:"500"`
    	Message string `json:"message" example:"Internal Server Error"`
    }
    
    // ตัวอย่าง error response กรณี error อื่นๆ เช่น คิวรี่ผิดพลาด
    type Error500 struct {
    	Status    uint         `json:"status" example:"500"`
    	Error     ErrorData500 `json:"error"`
    	RequestId string       `json:"requestId" example:"3b6272b9-1ef1-45e0"`
    }
    
  • Response struct เอาไว้เป็นตัวอย่างข้อมูล response กรณีทำงานสำเร็จ ซึ่งโครงสร้าง json จะเหมือนกัน เลยจะสร้างเอาไว้ใน pkg/common/swagdto/response.go

    pkg/common/swagdto/response.go
    package swagdto
    
    type PagingResult struct {
    	Page      int `json:"page" example:"1"`
    	Limit     int `json:"limit" example:"10"`
    	PrevPage  int `json:"prevPage" example:"0"`
    	NextPage  int `json:"nextPage" example:"2"`
    	Count     int `json:"count" example:"20"`
    	TotalPage int `json:"totalPage" example:"2"`
    }
    
    type Response struct {
    	Status    int         `json:"status" example:"200"`
    	Data      interface{} `json:"data,omitempty"`
    	RequestId string      `json:"requestId" example:"3b6272b9-1ef1-45e0"`
    }
    
    // reponse สำหรับการค้นหาแบบ pagination
    type ResponseWithPage struct {
    	Status     int          `json:"status" example:"200"`
    	Data       interface{}  `json:"data,omitempty"`
    	Pagination PagingResult `json:"_pagination,omitempty"`
    	RequestId  string       `json:"requestId" example:"3b6272b9-1ef1-45e0"`
    }
    
  • Data struct เป็นตัวอย่างข้อมูลใน response ซึ่งจะเปลี่ยนไปในแต่ละโมดูล ดังนั้นจะเขียนไว้ใน pkg/module/todo/swagger/todo.go

    pkg/module/todo/swagger/todo.go
    package swagger
    
    // ตัวอย่างข้อมูลที่ตอบกลับไป
    type TodoRepsonse struct {
    	ID        string `json:"id" example:"bfbc2a69-9825-4a0e-a8d6-ffb985dc719c"`
    	Text      string `json:"text" example:"do something"`
    	Completed bool   `json:"completed" example:"false"`
    }
    
    type ListTodoRepsonse []TodoRepsonse
    
    // กรณีตอบกลับแบบรายการเดียว ใน response.data จะได้ object ชื่อว่า todo
    type TodoSampleData struct {
    	Data TodoRepsonse `json:"todo"`
    }
    
    // กรณีตอบกลับแบบหลายรายการ ใน response.data จะได้ object ชื่อว่า todos เป็น array
    type TodoSampleListData struct {
    	Data ListTodoRepsonse `json:"todos"`
    }
    
    // ตัวอย่าง json body สำหรับส่งมาสร้างรายการใหม่
    // ใส่ comment Required: true เพื่อบอกว่าเป็น required field
    type CreateTodoFrom struct {
    	// Required: true
    	Text string `json:"text" example:"do something"`
    }
    
    // ตัวอย่าง json body สำหรับส่งมาอัพเดทสถานะ
    // ใส่ comment Required: true เพื่อบอกว่าเป็น required field
    type UpdateTodoStatusForm struct {
    	// Required: true
    	Completed bool `json:"completed"`
    }
    
    // กรณี validate create form ไม่ผ่าน
    type ErrorDetailCreate struct {
    	Target  string `json:"target" example:"text"`
    	Message string `json:"message" example:"text field is required"`
    }
    
    type ErrCreateSampleData struct {
    	Code    string              `json:"code" example:"422"`
    	Message string              `json:"message" example:"invalid data see details"`
    	Details []ErrorDetailCreate `json:"details"`
    }
    
    // กรณี validate update form ไม่ผ่าน
    type ErrorDetailUpdate struct {
    	Target  string `json:"target" example:"completed"`
    	Message string `json:"message" example:"completed field is required"`
    }
    
    type ErrUpdateSampleData struct {
    	Code    string              `json:"code" example:"422"`
    	Message string              `json:"message" example:"invalid data see details"`
    	Details []ErrorDetailUpdate `json:"details"`
    }
    
    

ตัวอย่างการใช้งาน

  • POST - /api/v1/todos สำหรับสร้างรายการใหม่
// @Summary Add a new todo
// @Description Add a new todo
// @Tags Todo
// @Accept  json
// @Produce  json
// @Param todo body swagger.CreateTodoFrom true "Todo Data"
// @Failure 422 {object} swagdto.Error422{error=swagger.ErrCreateSampleData}
// @Failure 500 {object} swagdto.Error500
// @Success 201 {object} swagdto.Response{data=swagger.TodoSampleData}
// @Router /todos [post]
func (h TodoHandler) CreateTodo(c common.HContext) error {
	// ...
}
  • GET - /api/v1/todos สำหรับค้นหารายการทั้งหมด
// @Summary List all existing todos
// @Description You can filter all existing todos by listing them.
// @Tags Todo
// @Accept  json
// @Produce  json
// @Param term query string false "filter the text based value (ex: term=dosomething)"
// @Param completed query bool false "filter the status based value (ex: completed=true)"
// @Param page query int false "Go to a specific page number. Start with 1"
// @Param limit query int false "Page size for the data"
// @Param order query string false "Page order. Eg: text desc,createdAt desc"
// @Failure 400 {object} swagdto.Error400
// @Failure 500 {object} swagdto.Error500
// @Success 200 {object} swagdto.ResponseWithPage{data=swagger.TodoSampleListData}
// @Router /todos [get]
func (h TodoHandler) ListTodo(c common.HContext) error {
  // ...
}
  • GET - /api/v1/todos/:id สำหรับค้นหารายการจาก id ที่ระบุ
// @Summary Get a todo
// @Description Get a specific todo by id
// @Produce json
// @Tags Todo
// @Param id path string true "Todo ID"
// @Failure 400 {object} swagdto.Error400
// @Failure 404 {object} swagdto.Error404
// @Failure 500 {object} swagdto.Error500
// @Success 200 {object} swagdto.Response{data=swagger.TodoSampleData}
// @Router /todos/{id} [get]
func (h TodoHandler) GetTodo(c common.HContext) error {
  // ...
}
  • PATCH - /api/v1/todos/:id สำหรับอัพเดทสถานะ รายการจาก id ที่ระบุ
// @Summary Update a todo status
// @Description Update a specific todo status by id
// @Produce json
// @Tags Todo
// @Param id path string true "Todo ID"
// @Param todo body swagger.UpdateTodoStatusForm true "Todo Status Data"
// @Failure 400 {object} swagdto.Error400
// @Failure 404 {object} swagdto.Error404
// @Failure 422 {object} swagdto.Error422{error=swagger.ErrUpdateSampleData}
// @Failure 500 {object} swagdto.Error500
// @Success 200 {object} swagdto.Response{data=swagger.TodoSampleData}
// @Router /todos/{id} [patch]
func (h TodoHandler) UpdateTodoStatus(c common.HContext) error {
  // ...
}
  • DELETE - /api/v1/todos/:id สำหรับลบรายการจาก id ที่ระบุ
// @Summary Delete a todo
// @Description Delete a specific todo by id
// @Produce  json
// @Tags Todo
// @Param id path string true "Todo ID"
// @Failure 400 {object} swagdto.Error400
// @Failure 404 {object} swagdto.Error404
// @Failure 500 {object} swagdto.Error500
// @Success 204
// @Router /todos/{id} [delete]
func (h TodoHandler) DeleteTodo(c common.HContext) error {
  // ...
}
  • รันคำสั่ง ให้ generate pkg/docs/doc.go ใหม่ จาก comments ที่เพิ่มเข้าไป
swag init -g pkg/module/module.go -o pkg/docs
Doc

สามารถดูโค้ดทั้งหมดได้ที่นี่