基本的な実装方法について
公式ドキュメントから転記
package main
import (
"net/http"
"strconv"
"github.com/labstack/echo"
"github.com/labstack/echo/engine/standard"
"github.com/labstack/echo/middleware"
)
type (
user struct {
ID int `json:"id"`
Name string `json:"name"`
}
)
var (
users = map[int]*user{}
seq = 1
)
//----------
// Handlers
//----------
func createUser(c echo.Context) error {
u := &user{
ID: seq,
}
if err := c.Bind(u); err != nil {
return err
}
users[u.ID] = u
seq++
return c.JSON(http.StatusCreated, u)
}
func getUser(c echo.Context) error {
id, _ := strconv.Atoi(c.Param("id"))
return c.JSON(http.StatusOK, users[id])
}
func updateUser(c echo.Context) error {
u := new(user)
if err := c.Bind(u); err != nil {
return err
}
id, _ := strconv.Atoi(c.Param("id"))
users[id].Name = u.Name
return c.JSON(http.StatusOK, users[id])
}
func deleteUser(c echo.Context) error {
id, _ := strconv.Atoi(c.Param("id"))
delete(users, id)
return c.NoContent(http.StatusNoContent)
}
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.POST("/users", createUser)
e.GET("/users/:id", getUser)
e.PUT("/users/:id", updateUser)
e.DELETE("/users/:id", deleteUser)
// Start server
e.Run(standard.New(":1323"))
}
これでOKではない!!
これはあくまでも、Routingの仕方とHandlerの実装方法を書いているだけで例外パターンは想定されていません。
実際にサービスを作るときにはこれだけでは足りないのです。
ではどうするか
1. エラーハンドラをカスタマイズする
APIサーバを構築する場合、正常でも異常でも同じ形式で結果を返すべきだと考えます。
(正常系がJSONで返すなら、異常でもJSONを返すべき)
しかし、デフォルトのエラーハンドラはtext形式 (echo.Context.String) で
エラーレスポンスを返します。
これは、echoが全ての形式を統一するのではなく、ハンドラ毎にレスポンスの形式を設定できるようになっているからです。
例えば、公式ドキュメントの使用例の場合
{
name: "foobar"
}
などになるのに対して、エラーの場合、次のようになります。
Not Found
これを解消するためにカスタマイズしたエラーハンドラを設定します。
type APIError struct {
Code int
Message string
}
func JSONErrorHandler(err error, context *echo.Context) {
code := http.StatusInternalServerError
msg := http.StatusText(code)
if he, ok := err.(*HTTPError); ok {
code = he.Code
msg = he.Message
}
if e.debug {
msg = err.Error()
}
var apierr APIError
apierr.Code = code
apierr.Message = msg
if !c.Response().Committed() {
c.JSON(code, apierr)
}
e.logger.Debug(err)
}
これをRouter( main() )で設定します。
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// 今回追加したところ
e.SetHTTPErrorHandler(JSONErrorHandler)
// Routes
e.POST("/users", createUser)
e.GET("/users/:id", getUser)
e.PUT("/users/:id", updateUser)
e.DELETE("/users/:id", deleteUser)
// Start server
e.Run(standard.New(":1323"))
}
2. エラーは error
で返すだけでは不十分
エラーを返す場合は
if err := c.Bind(u); err != nil {
return err
}
のような箇所を
if err := c.Bind(u); err != nil {
var apierr APIError
apierr.Code = 100
apierr.Message = "invalid request"
c.JSON(htt.StatusBadRequest, apierr)
return err
}
とするか
もしくはWrapperを作った方がいいです
func JSONError(c *echo.Context, status int err error, code int, msg string) error {
var apierr APIError
apierr.Code = code
apierr.Message = msg
c.JSON(status, apierr)
return err
}
if err := c.Bind(u); err != nil {
return JSONError
}
先ほどの作ったエラーハンドラでは、HTTP Statusからcodeとmessageを生成していました。
この場合であれば、先ほどのエラーハンドラでだけでも問題ありません。
しかし、実際APIを作る場合は独自のエラーコードとメッセージを返したり、
ユーザ向けのエラーメッセージとその詳細を返す場合があります。
この場合だと、先ほど作ったエラーハンドラでは処理が足りません。
エラー毎にきちんとエラーレスポンスを書くのにはまだ利点があります。
仮にエラーハンドラ内で独自のエラーコードや詳細など付随する情報を自動生成したとしても
特定のエラーの場合には例外的に通常とは異なるエラーを返したい場合があります。
このような場合に柔軟に対応するためにも個別にエラーレスポンスを返せるようにしておいたほうがいいのです。
もう一つ注目すべきなのが改修後も err
を返しているところです。
これは echo.Context.JSON
や echo.Context.String
がHTTP Statusに関係なく nil
を返してしまい、Unitテストの弊害になってしまうためです。
Unitテストに関しては別の機会に書きます。
labstack/echo の構築例はパターンが多く、とても良く書かれていますが、
それでも機能説明に最低限のところしか書かれていません。
実際に使用する場合は関数をよく理解したうえで使いましょう。