# Go Echo フレームワーク 本番運用ガイド：v5 の新API・ルーティング・Context・グレースフルシャットダウンで落ちないAPIを作る

> Go の Echo フレームワークを本番品質で運用する実装ガイド。公式ドキュメント（v5）に忠実に、v4→v5 の破壊的変更（*echo.Context・slog・StartConfig）、ルーティング、Context、バインディングとバリデーション、集中エラーハンドリング、グレースフルシャットダウン、テスト、Dockerデプロイ、Echo採用判断までを実コードで解説します。

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: Go, Echo, アーキテクチャ設計, 型安全, 可観測性, セキュリティ
- URL: https://tomodahinata.com/blog/go-echo-framework-production-guide
- カテゴリ: Go・Echo 本番運用

## 要点

- Echo v5 はハンドラ署名が func(c *echo.Context) error に変わった（Context がインターフェースから構造体ポインタへ）。v4 のコードはそのままでは動かない
- ロガーは log/slog に統一され、middleware.Logger と Timeout はコアから削除。v4 は 2026-12-31 までのサポートで、新規は v5、既存は移行計画を前提に
- 本番の肝はグレースフルシャットダウン。v5 は e.Shutdown() を廃し、StartConfig{GracefulTimeout} に signal.NotifyContext を渡す形に一本化された
- 外部入力は DTO に Bind→Validate して境界で殺す。エラーは握りつぶさず return し、集中 HTTPErrorHandler で一貫した JSON に変換する
- Echo は net/http 互換の薄い層。Controller/UseCase/Repository に切り、DI（google/wire 等）でハンドラを純粋に保てば、テストも差し替えも容易になる

---

「Go で API を立てたい。`net/http` は薄すぎるし、フルスタックは重い」——その中間にいるのが **Echo** です。`@app.get` 一行で動く軽さと、本番に必要な部品（ルーター・ミドルウェア・バインディング・集中エラー処理）が最初から揃っています。けれど **本番** に載せた瞬間、判断すべきことが一気に増えます。**どのバージョンを使うのか。リクエストをどう検証するのか。panic で全体が落ちないか。デプロイ時に処理中のリクエストを取りこぼさないか。落ちたとき、ログから追えるのか。**

この記事は、Echo を **本番品質**で運用するための実装ガイドです。公式チュートリアルが「動かす」ところまでを教えてくれるのに対し、ここでは「**落ちない・追える・変更しやすい**」を作るための判断基準とコードに焦点を当てます。題材として、私が実際に Go/Echo でバックエンドを構築した[外国人旅行客向け飲食店マッチングサイト](/case-studies/restaurant-matching)（Echo + `google/wire` でクリーンアーキテクチャ、Controller/UseCase/Repository 分離、`go test`／`golangci-lint` を CI で強制）での設計判断も交えます。

> **この記事のルール**：API 仕様・推奨パターンは **Echo 公式ドキュメント（v5・2026年6月時点）** に基づきます。Echo `v5` は現在の安定ライン、`v4` は **セキュリティ修正とバグ修正が 2026-12-31 まで**提供されます。仕様は改定されるため、本番投入前に必ず[公式ドキュメント](https://echo.labstack.com/)で最新の挙動を確認してください。コードは実運用で使える形に整えていますが、**シークレット（署名鍵・DB URL・APIキー）は環境変数前提**です（ハードコード厳禁）。

---

## 0. なぜ Echo なのか——3行で押さえる立ち位置

設計判断の前に、Echo が何の上に立っているかを押さえます。公式は次の3点を土台に挙げています。

- **`net/http` の上の薄い層**：Echo は標準ライブラリの `net/http` を土台にした「高性能・ミニマリスト」フレームワーク。Go の標準的な作法から大きく外れません。
- **最適化された HTTP ルーター**：静的・パラメータ・ワイルドカードを賢く優先順位付けする radix tree ベースのルーター。ゼロアロケーションに近い経路探索が売りです。
- **電池付き（batteries-included）**：ミドルウェア、JSON/XML/フォームのデータバインディング、テンプレートレンダリング、集中エラーハンドリング、Let's Encrypt による自動 TLS、HTTP/2 を最初から備えます。

つまり Echo の価値は、**「`net/http` の素朴さ」と「本番に必要な部品」のちょうど中間**にあります。Gin に比べてミドルウェアの設計が一貫しており、Fiber（`fasthttp` ベース）と違って `net/http` 互換なので標準エコシステム（`context`、`http.Handler`、各種ミドルウェア）をそのまま使えます。採用判断の詳細は[最終章](#12-echo-vs-gin-vs-fiber-vs-net-http採用判断)で扱います。

---

## 1. 最重要：v5 で「何が変わったか」を最初に押さえる

ここを飛ばすと、ネット上の v4 サンプルをコピペして**コンパイルが通らない**、あるいは**微妙に挙動が違う**状態に陥ります。Echo v5 は v4 から**破壊的変更**が入った節目のリリースです。まず差分を頭に入れてから書き始めましょう。

### 1.1 最小サーバー（v5 公式の Hello World）

```go
package main

import (
	"log/slog"
	"net/http"

	"github.com/labstack/echo/v5"
	"github.com/labstack/echo/v5/middleware"
)

// ハンドラ：v5 では *echo.Context（構造体ポインタ）を受け取る
func hello(c *echo.Context) error {
	return c.String(http.StatusOK, "Hello, World!")
}

func main() {
	e := echo.New()

	// ミドルウェア
	e.Use(middleware.RequestLogger()) // v4 の middleware.Logger() は廃止
	e.Use(middleware.Recover())       // panic を回復し集中エラーハンドラへ

	// ルート
	e.GET("/", hello)

	// 起動
	if err := e.Start(":8080"); err != nil {
		slog.Error("failed to start server", "error", err)
	}
}
```

インストールは次の通り。Echo v5 は **Go 1.25 以上**（直近4つの Go メジャーリリース）を要求します。

```bash
go get github.com/labstack/echo/v5
```

### 1.2 v4 → v5 の決定的な差分

| 項目 | v4 | v5 | 影響 |
| --- | --- | --- | --- |
| ハンドラ署名 | `func(c echo.Context) error`（**interface**） | `func(c *echo.Context) error`（**struct ポインタ**） | **全ハンドラ・全ミドルウェアの修正が必要** |
| ロギング | 独自 `Logger` インターフェース | 標準の `log/slog`（`e.Logger` は `*slog.Logger`） | 構造化ログが標準に |
| リクエストログ | `middleware.Logger()` | `middleware.RequestLogger()` | 旧 `Logger` はコアから削除 |
| タイムアウト | `middleware.Timeout()` | **コアから削除** | `http.Server` のタイムアウト/`context` で代替 |
| グレースフル停止 | `e.Shutdown(ctx)` / `e.Close()` | `StartConfig{GracefulTimeout}` に集約 | 起動コードの書き換えが必要 |
| ルート戻り値 | `*echo.Route` | `RouteInfo`（`Routes` コレクションで絞り込み） | 逆引きルーティングの書き方が変化 |
| `HTTPError.Message` | `interface{}` | `string`（`NewHTTPError(code, msg)` 非可変長） | メッセージ整形の方針が明確に |
| エラーハンドラ署名 | `func(err error, c echo.Context)` | `func(c *echo.Context, err error)` | 引数順が反転 |
| `c.Response()` | `*echo.Response` | `http.ResponseWriter`（`echo.UnwrapResponse` で取得） | 低レベル操作の書き方が変化 |
| バインダ | `FormParam*` | `FormValue*`、`Bind(c, target)` に署名反転 | フォーム系 API 名が変化 |
| 型安全抽出 | `ValueBinder` | ジェネリクス（`PathParam[T]`/`QueryParam[T]`） | コンパイル時型安全が前進 |
| `echo.Map` | あり | **削除** | `map[string]any` を直接使う |
| 定数 | `echo.CONNECT` 等 | 削除 → `http.MethodConnect` 等 | 標準定数へ寄せる |
| JWT | `echo-jwt/v4` | `echo-jwt/v5`（コア外の別モジュール） | import 変更 |
| テスト | `e.NewContext` | `e.NewContext` + 新 `echotest` パッケージ | ボイラープレート削減 |

> **移行の現実的な指針**：新規プロジェクトは **v5** を選びます。既存の v4 プロジェクトは、**2026-12-31 のサポート終了**を見据えて移行計画を立てつつ、急いで壊す必要はありません。`API_CHANGES_V5.md`（公式）が差分の一次情報です。本記事のコードはすべて **v5** で書いています。

---

## 2. ルーティング：静的 → パラメータ → ワイルドカードの優先順位

Echo のルーターは、**マッチの種類で優先順位が決まっている**のが肝です。これを理解すると「なぜこの経路が拾われたのか」で悩まなくなります。

```go
e := echo.New()

e.GET("/users/:id", getUser)        // パラメータ
e.POST("/users", createUser)
e.PUT("/users/:id", updateUser)
e.DELETE("/users/:id", deleteUser)
e.GET("/users/profile", profile)    // 静的（:id より優先される）
e.GET("/files/*", serveFiles)       // ワイルドカード
```

各メソッド（`GET`/`POST`/`PUT`/`DELETE`/`PATCH`…）の署名は次の通りで、戻り値は `RouteInfo` です。

```go
func (e *Echo) GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.RouteInfo
```

| パターン | 種類 | 例 | 優先 |
| --- | --- | --- | --- |
| `/users/profile` | 静的 | `/users/profile` | 1（最優先） |
| `/users/:id` | パラメータ | `/users/42` | 2 |
| `/files/*` | ワイルドカード | `/files/css/app.css` | 3 |

**「静的 → パラメータ → ワイルドカード」**の順で評価されるため、`/users/profile` と `/users/:id` を両方登録しても、`/users/profile` は意図通り静的ルートに吸われます。順序に依存しない決定的な動作です。

### 2.1 パスパラメータとワイルドカードの読み取り

```go
func getUser(c *echo.Context) error {
	id := c.Param("id") // "/users/42" → "42"
	return c.String(http.StatusOK, id)
}

// ワイルドカードは "*" で受ける
e.GET("/files/*", func(c *echo.Context) error {
	return c.String(http.StatusOK, c.Param("*")) // "/files/a/b.css" → "a/b.css"
})
```

文字列のままでは型安全ではありません。`"42"` のような数値 ID は、後述の[型安全バインダ](#41-クリーンな入力はビジネス構造体に直接bindしない)で `int64` に変換し、変換失敗を 400 に落とすのが本番の作法です（詳細は[バインディング＆バリデーションの記事](/blog/go-echo-request-binding-validation-error-handling-guide)）。

### 2.2 ルートグループ：横断的関心事を一箇所で束ねる

認証・バージョニング・テナント境界は、**グループ**で束ねるのが Echo 流です。プレフィックスとミドルウェアをまとめて適用できます。

```go
// /api/v1 配下に共通ミドルウェアを適用
api := e.Group("/api/v1")
api.Use(middleware.CORS("https://app.example.com"))

// 認証が必要な管理 API は、さらにネストしたグループに切る
admin := api.Group("/admin")
admin.Use(requireAdmin) // 自作の認可ミドルウェア
admin.GET("/metrics", metrics) // GET /api/v1/admin/metrics
admin.GET("/users", listUsers) // GET /api/v1/admin/users
```

グループはネスト可能で、**「公開 API」「認証必須 API」「管理者専用 API」**のように権限境界をディレクトリのように表現できます。ミドルウェアの適用レベル（ルート全体／グループ／個別ルート）と順序の設計は、[ミドルウェア完全ガイド](/blog/go-echo-middleware-cors-csrf-jwt-rate-limit-security-guide)で深掘りします。

---

## 3. Context：1つの `*echo.Context` で読み書きが完結する

Echo のハンドラは `*echo.Context` を1つ受け取り、そこから**リクエストの読み取り**と**レスポンスの書き込み**の両方を行います。v5 で構造体ポインタになったことで、ポインタの取り回しが標準的な Go の感覚に近づきました。

### 3.1 レスポンスの書き方

```go
func handler(c *echo.Context) error {
	// JSON（API で最頻出。構造体をそのまま渡す）
	return c.JSON(http.StatusOK, map[string]any{"ok": true})
}

// 用途別のレスポンスヘルパー
c.String(http.StatusOK, "plain text")
c.HTML(http.StatusOK, "<b>hi</b>")
c.XML(http.StatusOK, payload)
c.Blob(http.StatusOK, "application/pdf", pdfBytes)
c.Stream(http.StatusOK, "application/octet-stream", reader) // 大きな本文をストリーム
c.NoContent(http.StatusNoContent)                            // 204
c.Redirect(http.StatusFound, "/elsewhere")                   // 302
c.File("public/index.html")                                  // ファイルを直接配信
c.Attachment("report.csv", "report.csv")                     // ダウンロードを促す
```

### 3.2 リクエストの読み取りと「デフォルト付き」ヘルパー

```go
id := c.Param("id")                            // パスパラメータ
q := c.QueryParam("q")                         // クエリ文字列
name := c.FormValue("name")                    // フォーム値
ua := c.Request().Header.Get(echo.HeaderUserAgent) // ヘッダ

// 値がないときの既定値を一行で（v5 の利便メソッド）
page := c.QueryParamOr("page", "1")
sort := c.FormValueOr("sort", "created_at")
```

### 3.3 `Set`/`Get`：ミドルウェアからハンドラへ値を渡す

認証ミドルウェアで解決したユーザーや、リクエスト ID を**ハンドラへ受け渡す**ときに使います。これがミドルウェアとハンドラを疎結合に保つ要です。

```go
// ミドルウェア側：認証済みユーザーを詰める
func authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c *echo.Context) error {
		user := resolveUser(c) // トークン検証など
		c.Set("user", user)
		return next(c)
	}
}

// ハンドラ側：型アサーションで取り出す（境界で必ず ok を確認）
func me(c *echo.Context) error {
	user, ok := c.Get("user").(*User)
	if !ok {
		return echo.ErrUnauthorized
	}
	return c.JSON(http.StatusOK, user)
}
```

> **低レベル操作の注意**：v5 では `c.Response()` が `http.ResponseWriter` を返します。Echo 固有の `*Response`（`Committed` フラグなど）が必要なときは `echo.UnwrapResponse(c.Response())` で取り出します。集中エラーハンドラで「すでに送信済みか」を判定する場面で使います（[後述](#5-エラーハンドリングエラーは握りつぶさずreturnする)）。

---

## 4. データバインディングとバリデーション：境界で不正入力を殺す

> セキュリティ原則：**外部から来る入力は、信頼できる型に変換できるまで信用しない。** Echo はこの境界処理を `c.Bind` と `c.Validate` で薄く支援しますが、**設計の責任は実装者**にあります。

### 4.1 クリーンな入力はビジネス構造体に直接 `Bind` しない

`c.Bind(&dto)` は、パスパラメータ・クエリ・リクエストボディ（JSON/XML/フォーム）を構造体タグに従って流し込みます。

```go
// ❌ DB エンティティに直接 Bind すると、IsAdmin など意図しないフィールドまで上書きされ得る
// ✅ 受け口専用の DTO を定義し、検証してからドメインに移す
type CreateUserDTO struct {
	Name  string `json:"name"  validate:"required,min=2,max=50"`
	Email string `json:"email" validate:"required,email"`
}

func (h *UserHandler) Create(c *echo.Context) error {
	var dto CreateUserDTO
	if err := c.Bind(&dto); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "invalid request body")
	}
	if err := c.Validate(&dto); err != nil {
		return err // 集中エラーハンドラが 400/422 に整形
	}

	user, err := h.users.Create(c.Request().Context(), dto.Name, dto.Email)
	if err != nil {
		return err
	}
	return c.JSON(http.StatusCreated, user)
}
```

タグはソースごとに使い分けます（`json`/`xml`=ボディ、`query`=クエリ、`param`=パス、`form`=フォーム、`header`=ヘッダ）。**ヘッダは `c.Bind` の対象外**で、`echo.BindHeaders(c, &dto)` を明示的に呼びます。

### 4.2 バリデータの登録

Echo は**特定のバリデーションライブラリを強制しません**。デファクトの `github.com/go-playground/validator/v10` を `echo.Validator` インターフェースに被せて登録します。

```go
import "github.com/go-playground/validator/v10"

type CustomValidator struct {
	validator *validator.Validate
}

func (cv *CustomValidator) Validate(i any) error {
	if err := cv.validator.Struct(i); err != nil {
		// 422 にして「どのフィールドがなぜ落ちたか」を返すのが本番の作法
		return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
	}
	return nil
}

func main() {
	e := echo.New()
	e.Validator = &CustomValidator{validator: validator.New()}
	// ...
}
```

バリデーションエラーの構造化（フィールド単位の `422` レスポンス）、型安全な Fluent バインダ（`echo.QueryParamsBinder(c).Int64s(...)`）、カスタム `Binder` の実装は、**[バインディング＆バリデーション＆エラー設計の記事](/blog/go-echo-request-binding-validation-error-handling-guide)**で完全に扱います。

---

## 5. エラーハンドリング：エラーは握りつぶさず `return` する

Echo のエラー処理は**中央集権**です。ハンドラやミドルウェアは `error` を `return` するだけ。それを**1つの `HTTPErrorHandler`** が HTTP レスポンスに変換します。`if err != nil { c.JSON(...) ; return }` を各所に書く必要はありません。

```go
// ステータス付きエラーを返す
return echo.NewHTTPError(http.StatusUnauthorized, "Please provide valid credentials")

// メッセージ省略時はステータステキストが使われる
return echo.NewHTTPError(http.StatusNotFound)

// センチネルエラー（定義済み）も使える
return echo.ErrBadRequest // 400
return echo.ErrForbidden  // 403
```

プレーンな `error` を返すと `500 Internal Server Error` に、`*echo.HTTPError` はそのステータスとメッセージになります。デフォルトハンドラは `{"message": "..."}` の JSON を返します。

### 5.1 本番向けの集中エラーハンドラ

本番では、**内部エラーの詳細をクライアントに漏らさない**・**ログには full な情報を残す**・**一貫した JSON 形式で返す**ハンドラに差し替えます。v5 の署名は `func(c *echo.Context, err error)`（**引数順が v4 から反転**）です。

```go
func customHTTPErrorHandler(c *echo.Context, err error) {
	// すでにレスポンス送信済みなら二重送信しない
	if resp, uErr := echo.UnwrapResponse(c.Response()); uErr == nil && resp.Committed {
		return
	}

	// ステータスコードの解決（HTTPError 等は HTTPStatusCoder を実装している）
	code := http.StatusInternalServerError
	var sc echo.HTTPStatusCoder
	if errors.As(err, &sc) {
		if s := sc.StatusCode(); s != 0 {
			code = s
		}
	}

	// 5xx は詳細を隠し、相関 ID 付きでログへ。4xx はメッセージを返す
	if code >= 500 {
		c.Logger().Error("unhandled error",
			"error", err.Error(),
			"path", c.Request().URL.Path,
			"request_id", c.Response().Header().Get(echo.HeaderXRequestID),
		)
		_ = c.JSON(code, map[string]string{"error": "internal server error"})
		return
	}
	_ = c.JSON(code, map[string]string{"error": http.StatusText(code)})
}

func main() {
	e := echo.New()
	e.HTTPErrorHandler = customHTTPErrorHandler
	// ...
}
```

> 公式は「エラーを Sentry / Elasticsearch / Splunk 等へ転送するのも、この集中ハンドラから行うのが良い」と述べています。ログ送出・メトリクス計上・通知を**一箇所に集約**できるのが中央集権エラー処理の最大の利点です。Problem Details（RFC 9457）形式での返却や、バリデーションエラーの整形は[エラー設計の記事](/blog/go-echo-request-binding-validation-error-handling-guide)で扱います。

---

## 6. ミドルウェア：適用レベルと順序がすべて

ミドルウェアは `func(echo.HandlerFunc) echo.HandlerFunc` で、**リクエストの前後**に処理を挟みます。適用レベルは3つ。

```go
e.Use(middleware.Recover())                 // ① 全ルートに適用（最外周）
api := e.Group("/api", middleware.CORS(...)) // ② グループ単位
e.GET("/admin", handler, requireAdmin)       // ③ 個別ルート
```

本番で最初に入れるべき定番は次の通りです（**順序が意味を持つ**ことに注意）。

```go
e := echo.New()
e.Use(middleware.Recover())        // panic を最外周で回収（最初に置く）
e.Use(middleware.RequestID())      // 相関 ID を採番
e.Use(middleware.RequestLogger())  // 構造化アクセスログ（slog）
e.Use(middleware.Secure())         // セキュリティヘッダ
e.Use(middleware.BodyLimit(2_097_152)) // 本文サイズ上限 2MB（DoS 緩和）
e.Use(middleware.Gzip())           // レスポンス圧縮
```

`middleware.Recover()` は**チェーンのどこで panic しても回収**してスタックトレースを出力し、制御を集中エラーハンドラへ渡します。だから panic でプロセス全体が死にません。CORS・CSRF・Secure・RateLimiter・JWT/KeyAuth・カスタムミドルウェアの設定と、**「どの順で並べるべきか」**は、独立した記事 **[Echo ミドルウェア完全ガイド](/blog/go-echo-middleware-cors-csrf-jwt-rate-limit-security-guide)** で網羅します。

> **v5 の注意**：`middleware.Logger()` と `middleware.Timeout()` はコアから**削除**されました。アクセスログは `RequestLogger()`、リクエストのタイムアウトは `http.Server` のタイムアウトや `context.WithTimeout`（ハンドラ内）で実現します。

---

## 7. グレースフルシャットダウン：本番で最も差がつく一手

デプロイのたびにプロセスは入れ替わります。このとき**処理中のリクエストを途中で切る**と、ユーザーには 502 が、決済なら不整合が生まれます。Kubernetes / ECS / Cloud Run はどれも、停止時にまず **`SIGTERM`** を送って「猶予のうちに片付けて」と告げます。これを正しく受けるのが本番の必須要件です。

v5 はこの作法を **`StartConfig`** に一本化しました（v4 の `e.Shutdown()` / `e.Close()` は廃止）。`signal.NotifyContext` で `SIGTERM`/`SIGINT` を待ち受けるコンテキストを作り、`StartConfig.Start(ctx, e)` に渡すだけです。

```go
package main

import (
	"context"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/labstack/echo/v5"
	"github.com/labstack/echo/v5/middleware"
)

func main() {
	e := echo.New()
	e.Use(middleware.Recover())
	e.Use(middleware.RequestLogger())

	e.GET("/slow", func(c *echo.Context) error {
		time.Sleep(5 * time.Second) // 処理中のリクエストを想定
		return c.JSON(http.StatusOK, map[string]string{"status": "done"})
	})

	// SIGINT / SIGTERM を受けると ctx がキャンセルされる
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	sc := echo.StartConfig{
		Address:         ":8080",
		GracefulTimeout: 10 * time.Second, // 既存リクエストの完了を最大10秒待つ
	}
	if err := sc.Start(ctx, e); err != nil {
		e.Logger.Error("server stopped", "error", err)
	}
}
```

`SIGTERM` が来ると、Echo は**新規接続の受付を止め**、`GracefulTimeout` の範囲で処理中のリクエストの完了を待ってから終了します。`StartConfig` には TLS 設定やライフサイクルコールバックのフィールドもあり、`Start`/`StartTLS`/`StartAutoTLS` を個別に呼んでいた v4 の起動コードがここに集約されています。

> **運用の整合**：`GracefulTimeout` は、オーケストレータの**終了猶予（`terminationGracePeriodSeconds` 等）より短く**設定します。逆だと、片付けが終わる前に `SIGKILL` で強制終了されます。コンテナ環境での `SIGTERM`・冪等性・ゼロダウンタイムの設計は、[ECS/Fargate の本番ガイド](/blog/aws-ecs-fargate-production-guide)や[Azure Container Apps の本番ガイド](/blog/azure-container-apps-production-guide)とも地続きです。

---

## 8. 可観測性：slog 構造化ログ + 相関 ID

「落ちたとき追えるか」は、ログを **構造化** し、**相関 ID** で1リクエストを貫通させられるかで決まります。v5 はロガーを標準の `log/slog` に統一したので、JSON ログが標準で得られます。

```go
import (
	"context"
	"log/slog"
	"os"
)

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
	LogStatus:   true,
	LogURI:      true,
	LogError:    true,
	HandleError: true, // エラーを集中ハンドラへ正しく伝播させる
	LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error {
		if v.Error == nil {
			logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST",
				slog.String("uri", v.URI),
				slog.Int("status", v.Status),
			)
		} else {
			logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR",
				slog.String("uri", v.URI),
				slog.Int("status", v.Status),
				slog.String("err", v.Error.Error()),
			)
		}
		return nil
	},
}))
```

`RequestLoggerValues` には `Status`/`URI`/`Error`/`Latency`/`RemoteIP` などが入ります。`middleware.RequestID()` と組み合わせ、各ログ行に `request_id` を載せれば、フロントの 1 エラーからバックエンドのログ・トレースまで一本の糸で辿れます。OpenTelemetry によるトレース・メトリクスへの拡張は Echo 専用に[Echo の可観測性（OpenTelemetry）の記事](/blog/go-echo-opentelemetry-distributed-tracing-metrics-observability-guide)で、プラットフォーム横断の設計は[可観測性の実践ガイド](/blog/opentelemetry-observability-production-tracing-metrics-logs)で扱います。ファイルアップロード（[S3・presigned URL](/blog/go-echo-file-upload-multipart-s3-streaming-presigned-url-guide)）、リアルタイム（[WebSocket/SSE](/blog/go-echo-websocket-sse-realtime-streaming-guide)）、API ドキュメント（[OpenAPI/Swagger](/blog/go-echo-openapi-swagger-swag-oapi-codegen-documentation-guide)）も各論で深掘りしています。

---

## 9. アーキテクチャ：ハンドラを薄く保ち、DI でテスト可能にする

Echo はあくまで **HTTP の入出力層**です。ビジネスロジックをハンドラに書き込むと、テストが書けず、変更が怖くなります。私が[飲食店マッチングサイト](/case-studies/restaurant-matching)のバックエンドで採った構成は、Go の定石である**クリーンアーキテクチャ + DI** です。

- **Controller（Echo ハンドラ）**：HTTP の入出力だけ。Bind/Validate して UseCase を呼び、結果を JSON にする。
- **UseCase**：アプリケーションのユースケース。HTTP も DB も知らない純粋なロジック。
- **Repository**：永続化の抽象。インターフェースで定義し、実装（PostgreSQL 等）を差し替え可能にする。
- **DI（`google/wire`）**：依存の配線をコンパイル時に解決。ハンドラに具象を直接持たせない（依存性逆転）。

```go
// Repository はインターフェースで定義（依存性逆転）
type UserRepository interface {
	Create(ctx context.Context, name, email string) (*User, error)
}

// UseCase は Repository に依存（具象 DB を知らない）
type UserUseCase struct {
	repo UserRepository
}

func (u *UserUseCase) Register(ctx context.Context, name, email string) (*User, error) {
	// バリデーション済み前提のドメインロジックだけ
	return u.repo.Create(ctx, name, email)
}

// Controller は UseCase に依存（HTTP の作法だけを知る）
type UserHandler struct {
	uc *UserUseCase
}

func (h *UserHandler) Create(c *echo.Context) error {
	var dto CreateUserDTO
	if err := c.Bind(&dto); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "invalid request body")
	}
	if err := c.Validate(&dto); err != nil {
		return err
	}
	user, err := h.uc.Register(c.Request().Context(), dto.Name, dto.Email)
	if err != nil {
		return err
	}
	return c.JSON(http.StatusCreated, user)
}
```

この分離の見返りは**テスト容易性**です。`UserRepository` をモックに差し替えれば、UseCase は DB なしで網羅的に単体テストできます。`google/wire` で配線をコード生成すれば、手書きの依存注入のミスもなくなります。層の切り方・依存性逆転・wire の使い方は[クリーンアーキテクチャ + DI の記事](/blog/go-echo-clean-architecture-dependency-injection-google-wire-guide)で、Repository 実装（pgx/sqlc/GORM・トランザクション・接続プール）は[データベース層の記事](/blog/go-echo-database-postgresql-pgx-sqlc-gorm-transaction-guide)で、認証・認可（JWT 発行/検証・RBAC・リフレッシュトークン）は[認証・認可の記事](/blog/go-echo-jwt-authentication-authorization-rbac-refresh-token-guide)で深掘りしています。

---

## 10. テスト：ハンドラは「Context を渡すだけ」で検証できる

Echo のハンドラは `*echo.Context` を受け取る素の関数なので、`net/http/httptest` でそのままテストできます。これが「テストしやすさ」の源泉です。

```go
func TestUserHandler_Create(t *testing.T) {
	e := echo.New()
	e.Validator = &CustomValidator{validator: validator.New()}

	body := `{"name":"Hinata","email":"hinata@example.com"}`
	req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
	req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
	rec := httptest.NewRecorder()
	c := e.NewContext(req, rec)

	h := &UserHandler{uc: newUseCaseWithMockRepo()} // Repository はモック

	if assert.NoError(t, h.Create(c)) {
		assert.Equal(t, http.StatusCreated, rec.Code)
	}
}
```

v5 には新しい **`echotest`** パッケージがあり、ボイラープレートをさらに減らせます。

```go
import "github.com/labstack/echo/v5/echotest"

func TestUserHandler_Create_WithHelper(t *testing.T) {
	h := &UserHandler{uc: newUseCaseWithMockRepo()}

	rec := echotest.ContextConfig{
		Headers: map[string][]string{
			echo.HeaderContentType: {echo.MIMEApplicationJSON},
		},
		JSONBody: []byte(`{"name":"Hinata","email":"hinata@example.com"}`),
	}.ServeWithHandler(t, h.Create)

	assert.Equal(t, http.StatusCreated, rec.Code)
}
```

パスパラメータは `ContextConfig` の `PathValues`、フォームやマルチパートも専用フィールドで指定できます。`go test ./... -race -cover` を CI（GitHub Actions）で必須化し、`golangci-lint` と組み合わせて品質ゲートにするのが定番です。テーブル駆動・モック・`testcontainers` による実 DB 統合テストまでの戦略は、[Echo テスト戦略の記事](/blog/go-echo-testing-strategy-httptest-echotest-testcontainers-guide)で体系化しています。

---

## 11. デプロイ：マルチステージ Docker で最小・安全に出す

Go の強みは**単一バイナリ**です。マルチステージビルドで「ビルド環境」と「実行環境」を分け、実行イメージを極小・最小権限にします。

```dockerfile
# --- build stage ---
FROM golang:1.25 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# 静的リンク・デバッグ情報除去で軽量化
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /app ./cmd/server

# --- runtime stage ---
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /app /app
USER nonroot:nonroot         # 非 root で実行（最小権限）
EXPOSE 8080
ENTRYPOINT ["/app"]
```

ポイントは3つ。**(1)** `distroless` でシェルもパッケージマネージャも無いイメージにし攻撃面を削る、**(2)** `nonroot` で実行する、**(3)** ポート・DB URL・署名鍵は**環境変数**から読む（イメージに焼かない）。`SIGTERM` を受けて[グレースフルに止まる](#7-グレースフルシャットダウン本番で最も差がつく一手)コードと合わせて初めて、ローリングデプロイで無停止になります。サーバータイムアウト（v5 は `BeforeServeFunc`）・ヘルスチェック・ECS/Cloud Run の作法まで含めた本番デプロイは、[デプロイ完全ガイド](/blog/go-echo-deployment-docker-distroless-ecs-cloud-run-graceful-shutdown-guide)で詳説します。

---

## 12. Echo vs Gin vs Fiber vs net/http：採用判断

「Echo を使うべきか」は、プロジェクトの制約で決めます。感覚ではなく軸で。

| 観点 | net/http（標準） | Echo | Gin | Fiber |
| --- | --- | --- | --- | --- |
| 基盤 | 標準 | `net/http` | `net/http` | `fasthttp`（非互換） |
| 学習コスト | 中（手で書く部分が多い） | 低〜中 | 低 | 低 |
| ミドルウェア | 自前 | 一貫した内蔵群 | 内蔵 + コミュニティ | 内蔵（Express 風） |
| 標準エコシステム互換 | ◎ | ◎ | ◎ | △（`http.Handler` 非互換） |
| エラー処理 | 自前 | 集中ハンドラ（強み） | 自前寄り | 集中ハンドラ |
| 向く場面 | 極小・依存を増やしたくない | **REST/JSON API を手早く本番品質で** | 既存資産・最大級の事例数 | 生スループット最優先 |

**判断の目安**：

- **REST/JSON の業務 API を、本番品質で素早く**作るなら **Echo** が中庸でよい選択。集中エラー処理とミドルウェアの一貫性が効きます。
- **依存を極限まで減らしたい**・**ごく小さなサービス**なら、Go 1.22+ の強化された `net/http`（メソッド付きパターンルーティング）だけで足りることもあります。
- **`fasthttp` の生スループットが要件**で、標準エコシステム非互換を許容できるなら Fiber。ただし `context.Context` や `http.Handler` 資産が使えない代償は大きい。

Echo・Gin・net/http・Fiber の公平な比較と、用途別の選定フレームワーク・移行方針は、独立記事の **[Echo vs Gin vs net/http 徹底比較](/blog/go-echo-vs-gin-framework-comparison-selection-migration-guide)** で深掘りしています。

> 私自身は、フロントを Next.js、バックエンドを Go/Echo で組む構成を採ってきました。OpenAPI を契約に据えてクライアント⇄サーバーの型整合をビルド時に保証する設計は、[Next.js × Go × OpenAPI の記事](/blog/nextjs-go-openapi-end-to-end-type-safety)で詳述しています。

---

## まとめ：Echo を「本番品質」に引き上げる7つの要点

1. **v5 の差分を最初に押さえる**——`*echo.Context`・`slog`・`StartConfig`。v4 サンプルは無修正では動かない。
2. **ルーティングは優先順位（静的→param→*）を理解**し、権限境界はグループで束ねる。
3. **入力は DTO に `Bind`→`Validate`**して境界で殺す。ビジネス構造体に直接バインドしない。
4. **エラーは握りつぶさず `return`**。集中 `HTTPErrorHandler` で一貫した JSON とログ送出に集約する。
5. **`Recover` を最外周に**置き、panic でプロセスを殺さない。
6. **`StartConfig{GracefulTimeout}` + `SIGTERM`** でローリングデプロイを無停止にする。
7. **ハンドラを薄く保ち DI でテスト可能に**。Echo は HTTP 層、ロジックは UseCase へ。

Echo は「軽さ」と「本番に必要な部品」の中間にいるからこそ、**設計者の判断がそのまま品質に出る**フレームワークです。この記事の型を土台に、[ミドルウェア](/blog/go-echo-middleware-cors-csrf-jwt-rate-limit-security-guide)と[バインディング＆バリデーション＆エラー設計](/blog/go-echo-request-binding-validation-error-handling-guide)を押さえれば、落ちない・追える・変更しやすい API に届きます。
