Skip to content

Go-DDDマーケットプレイスアプリケーション Echoフレームワーク実装の詳細解説

1. 概要

この文書では、Go-DDDマーケットプレイスアプリケーションにおけるEchoフレームワークの実装について詳しく解説します。Echoは高性能でミニマルなGoのWebフレームワークであり、RESTful APIの構築に適しています。

1.1 Echoフレームワークの役割

Echoフレームワークは以下の役割を担います:

  1. HTTPリクエスト/レスポンスの処理: クライアントからのHTTPリクエストを受け取り、適切なハンドラに転送し、レスポンスを返します。
  2. ルーティング: URLパスとHTTPメソッドに基づいて、リクエストを適切なハンドラ関数にマッピングします。
  3. ミドルウェア: リクエスト処理の前後に実行される機能(ロギング、認証、エラーハンドリングなど)を提供します。
  4. パラメータ取得: URLパス、クエリ文字列、リクエストボディからのパラメータ取得を簡素化します。
  5. レスポンス生成: JSON、XML、HTMLなど様々な形式でのレスポンス生成をサポートします。

1.2 アーキテクチャにおける位置づけ

以下の図は、Echoフレームワークのアーキテクチャにおける位置づけを示しています:

uml diagram

2. Echoフレームワークの実装

以下はEchoフレームワーク実装の概要を表すフロー図です:

uml diagram

2.1 基本設定

アプリケーションでは、Echoインスタンスの作成と基本設定を行います。

package main

import (
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

func main() {
    // Echoインスタンスの作成
    e := echo.New()

    // ミドルウェアの設定
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
    e.Use(middleware.CORS())

    // サーバー起動
    e.Start(":8080")
}

2.2 ルーティング設定

APIエンドポイントのルーティングを設定します。

package rest

import (
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
    "github.com/sklinkert/go-ddd/internal/application/services"
)

// SetupRouter configures the Echo router with all API endpoints
func SetupRouter(e *echo.Echo, productService *services.ProductService, sellerService *services.SellerService) {
    // ミドルウェアの設定
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
    e.Use(middleware.CORS())

    // APIバージョンプレフィックス
    api := e.Group("/api/v1")

    // 商品エンドポイント
    NewProductController(api, productService)

    // 出品者エンドポイント
    NewSellerController(api, sellerService)

    // ヘルスチェックエンドポイント
    api.GET("/health", func(c echo.Context) error {
        return c.JSON(200, map[string]string{"status": "ok"})
    })
}

3. コントローラーの実装

3.1 商品コントローラー

商品関連のエンドポイントを処理するコントローラーを実装します。

package rest

import (
    "github.com/labstack/echo/v4"
    "github.com/sklinkert/go-ddd/internal/application/command"
    "github.com/sklinkert/go-ddd/internal/application/services"
    "net/http"
)

type ProductController struct {
    productService *services.ProductService
}

func NewProductController(api *echo.Group, productService *services.ProductService) {
    controller := &ProductController{
        productService: productService,
    }

    api.POST("/products", controller.Create)
    api.GET("/products", controller.FindAll)
    api.GET("/products/:id", controller.ById)
    api.PUT("/products/:id", controller.Update)
}

// Create handles POST /products
func (c *ProductController) Create(ctx echo.Context) error {
    var req struct {
        Name        string  `json:"name"`
        Description string  `json:"description"`
        Price       float64 `json:"price"`
        SellerId    string  `json:"seller_id"`
    }

    if err := ctx.Bind(&req); err != nil {
        return ctx.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
    }

    cmd := command.CreateProductCommand{
        Name:        req.Name,
        Description: req.Description,
        Price:       req.Price,
        SellerId:    req.SellerId,
    }

    product, err := c.productService.Create(cmd)
    if err != nil {
        return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
    }

    return ctx.JSON(http.StatusCreated, product)
}

// 他のハンドラメソッド(FindAll, ById, Update)も同様に実装

3.2 出品者コントローラー

出品者関連のエンドポイントを処理するコントローラーを実装します。

package rest

import (
    "github.com/labstack/echo/v4"
    "github.com/sklinkert/go-ddd/internal/application/command"
    "github.com/sklinkert/go-ddd/internal/application/services"
    "net/http"
)

type SellerController struct {
    sellerService *services.SellerService
}

func NewSellerController(api *echo.Group, sellerService *services.SellerService) {
    controller := &SellerController{
        sellerService: sellerService,
    }

    api.POST("/sellers", controller.Create)
    api.GET("/sellers", controller.FindAll)
    api.GET("/sellers/:id", controller.ById)
    api.PUT("/sellers/:id/name", controller.UpdateName)
    api.PUT("/sellers/:id/email", controller.UpdateEmail)
    api.DELETE("/sellers/:id", controller.Delete)
}

// Create handles POST /sellers
func (c *SellerController) Create(ctx echo.Context) error {
    var req struct {
        Name  string `json:"name"`
        Email string `json:"email"`
    }

    if err := ctx.Bind(&req); err != nil {
        return ctx.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
    }

    cmd := command.CreateSellerCommand{
        Name:  req.Name,
        Email: req.Email,
    }

    seller, err := c.sellerService.Create(cmd)
    if err != nil {
        return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
    }

    return ctx.JSON(http.StatusCreated, seller)
}

// 他のハンドラメソッド(FindAll, ById, UpdateName, UpdateEmail, Delete)も同様に実装

4. ミドルウェアの実装

4.1 エラーハンドリングミドルウェア

アプリケーション全体で一貫したエラーレスポンスを提供するミドルウェアを実装します。

package rest

import (
    "github.com/labstack/echo/v4"
    "github.com/sklinkert/go-ddd/internal/application/common"
    "net/http"
)

// ErrorHandlerMiddleware はアプリケーションのエラーを適切なHTTPレスポンスに変換するミドルウェア
func ErrorHandlerMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        err := next(c)
        if err == nil {
            return nil
        }

        // エラーの種類に応じてHTTPステータスコードを決定
        var statusCode int
        var message string

        // AppErrorの場合
        if appErr, ok := err.(common.AppError); ok {
            switch appErr.Type {
            case common.NotFound:
                statusCode = http.StatusNotFound
                message = appErr.Message
            case common.ValidationError:
                statusCode = http.StatusBadRequest
                message = appErr.Message
            case common.DatabaseError:
                statusCode = http.StatusInternalServerError
                message = "データベースエラーが発生しました"
            default:
                statusCode = http.StatusInternalServerError
                message = appErr.Message
            }
        } else if httpErr, ok := err.(*echo.HTTPError); ok {
            // Echoの標準HTTPエラーの場合
            statusCode = httpErr.Code
            message = httpErr.Message.(string)
        } else {
            // その他のエラー
            statusCode = http.StatusInternalServerError
            message = "予期しないエラーが発生しました"
        }

        // JSONレスポンスを返す
        return c.JSON(statusCode, map[string]string{"error": message})
    }
}

4.2 認証ミドルウェア

JWTを使用した認証ミドルウェアを実装します。

package rest

import (
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

// SetupAuthMiddleware configures JWT authentication middleware
func SetupAuthMiddleware(e *echo.Echo) {
    // JWT設定
    config := middleware.JWTConfig{
        SigningKey: []byte("your-secret-key"),
        TokenLookup: "header:Authorization",
        AuthScheme: "Bearer",
    }

    // 認証が必要なルートグループ
    auth := e.Group("/api/v1/auth")
    auth.Use(middleware.JWTWithConfig(config))

    // 認証が必要なエンドポイントを追加
    auth.GET("/profile", getProfile)
}

// getProfile handles GET /api/v1/auth/profile
func getProfile(c echo.Context) error {
    // JWTからユーザー情報を取得
    user := c.Get("user").(*jwt.Token)
    claims := user.Claims.(jwt.MapClaims)
    userId := claims["id"].(string)

    // ユーザープロファイルを返す
    return c.JSON(http.StatusOK, map[string]string{
        "id": userId,
        "message": "認証済みユーザー",
    })
}

5. リクエスト/レスポンスの処理

5.1 リクエストバインディング

クライアントからのリクエストデータを構造体にバインドする方法を示します。

// JSONリクエストのバインド例
func (c *ProductController) Create(ctx echo.Context) error {
    var req struct {
        Name        string  `json:"name"`
        Description string  `json:"description"`
        Price       float64 `json:"price"`
        SellerId    string  `json:"seller_id"`
    }

    if err := ctx.Bind(&req); err != nil {
        return ctx.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
    }

    // 以降の処理...
}

// URLパラメータの取得例
func (c *ProductController) ById(ctx echo.Context) error {
    id := ctx.Param("id")

    product, err := c.productService.ById(id)
    if err != nil {
        return ctx.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
    }

    return ctx.JSON(http.StatusOK, product)
}

// クエリパラメータの取得例
func (c *ProductController) FindAll(ctx echo.Context) error {
    limit := ctx.QueryParam("limit")
    offset := ctx.QueryParam("offset")

    // limitとoffsetを整数に変換して使用
    // ...

    products, err := c.productService.FindAll()
    if err != nil {
        return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
    }

    return ctx.JSON(http.StatusOK, products)
}

5.2 レスポンス生成

様々な形式でのレスポンス生成方法を示します。

```go // JSONレスポンス func (c *ProductController) ById(ctx echo.Context) error { // ... return ctx.JSON(http.StatusOK, product) }

// エラーレスポンス func (c *ProductController) Update(ctx echo.Context) error { // ... if err != nil { return ctx.JSON(http.StatusInternalServerError, map[string]string{ "error": err.Error(), }) } // ... }

// カスタムHTTPステータスコード func (c *ProductController) Create(ctx echo.Context) error { // ... return ctx.JSON(http.StatusCreated, product) }