- Published on
API Service with Go: API Documents
- Authors
- Name
- Somprasong Damyos
- @somprasongd
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
เริ่มจากติดตั้ง Swag
go install github.com/swaggo/swag/cmd/swag@latest
รัน Swag เพื่อสร้างไฟล์ที่จำเป็น (
pkg/docs
folder และpkg/docs/doc.go
)swag init --parseDependency -g pkg/module/module.go -o pkg/docs
แก้ไข
pkg/module/module.go
เพื่อใช้ middleware ในการ generate doc ui ขึ้นมาpkg/module/module.gopackage 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) }
ทดสอบรัน และเปิด browser ไปที่ http://localhost:8080/swagger/index.html ก็จะได้ Swagger 2.0 Api documents
ใส่รายละเอียด
เพิ่มรายละเอียดให้แต่ละ 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.gopackage 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.gopackage 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.gopackage 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
- และเปิด browser ไปที่ http://localhost:8080/swagger/index.html ก็จะเห็นว่ามีการอัพเดทแล้ว
สามารถดูโค้ดทั้งหมดได้ที่นี่