「Go で API を立てたい。net/http は薄すぎるし、フルスタックは重い」——その中間にいるのが Echo です。@app.get 一行で動く軽さと、本番に必要な部品(ルーター・ミドルウェア・バインディング・集中エラー処理)が最初から揃っています。けれど 本番 に載せた瞬間、判断すべきことが一気に増えます。どのバージョンを使うのか。リクエストをどう検証するのか。panic で全体が落ちないか。デプロイ時に処理中のリクエストを取りこぼさないか。落ちたとき、ログから追えるのか。
この記事は、Echo を 本番品質で運用するための実装ガイドです。公式チュートリアルが「動かす」ところまでを教えてくれるのに対し、ここでは「落ちない・追える・変更しやすい」を作るための判断基準とコードに焦点を当てます。題材として、私が実際に Go/Echo でバックエンドを構築した外国人旅行客向け飲食店マッチングサイト(Echo + google/wire でクリーンアーキテクチャ、Controller/UseCase/Repository 分離、go test/golangci-lint を CI で強制)での設計判断も交えます。
この記事のルール:API 仕様・推奨パターンは Echo 公式ドキュメント(v5・2026年6月時点) に基づきます。Echo
v5は現在の安定ライン、v4は セキュリティ修正とバグ修正が 2026-12-31 まで提供されます。仕様は改定されるため、本番投入前に必ず公式ドキュメントで最新の挙動を確認してください。コードは実運用で使える形に整えていますが、シークレット(署名鍵・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、各種ミドルウェア)をそのまま使えます。採用判断の詳細は最終章で扱います。
1. 最重要:v5 で「何が変わったか」を最初に押さえる
ここを飛ばすと、ネット上の v4 サンプルをコピペしてコンパイルが通らない、あるいは微妙に挙動が違う状態に陥ります。Echo v5 は v4 から破壊的変更が入った節目のリリースです。まず差分を頭に入れてから書き始めましょう。
1.1 最小サーバー(v5 公式の Hello World)
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 メジャーリリース)を要求します。
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 のルーターは、マッチの種類で優先順位が決まっているのが肝です。これを理解すると「なぜこの経路が拾われたのか」で悩まなくなります。
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 です。
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 パスパラメータとワイルドカードの読み取り
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 は、後述の型安全バインダで int64 に変換し、変換失敗を 400 に落とすのが本番の作法です(詳細はバインディング&バリデーションの記事)。
2.2 ルートグループ:横断的関心事を一箇所で束ねる
認証・バージョニング・テナント境界は、グループで束ねるのが Echo 流です。プレフィックスとミドルウェアをまとめて適用できます。
// /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」**のように権限境界をディレクトリのように表現できます。ミドルウェアの適用レベル(ルート全体/グループ/個別ルート)と順序の設計は、ミドルウェア完全ガイドで深掘りします。
3. Context:1つの *echo.Context で読み書きが完結する
Echo のハンドラは *echo.Context を1つ受け取り、そこからリクエストの読み取りとレスポンスの書き込みの両方を行います。v5 で構造体ポインタになったことで、ポインタの取り回しが標準的な Go の感覚に近づきました。
3.1 レスポンスの書き方
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 リクエストの読み取りと「デフォルト付き」ヘルパー
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 をハンドラへ受け渡すときに使います。これがミドルウェアとハンドラを疎結合に保つ要です。
// ミドルウェア側:認証済みユーザーを詰める
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())で取り出します。集中エラーハンドラで「すでに送信済みか」を判定する場面で使います(後述)。
4. データバインディングとバリデーション:境界で不正入力を殺す
セキュリティ原則:外部から来る入力は、信頼できる型に変換できるまで信用しない。 Echo はこの境界処理を
c.Bindとc.Validateで薄く支援しますが、設計の責任は実装者にあります。
4.1 クリーンな入力はビジネス構造体に直接 Bind しない
c.Bind(&dto) は、パスパラメータ・クエリ・リクエストボディ(JSON/XML/フォーム)を構造体タグに従って流し込みます。
// ❌ 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 インターフェースに被せて登録します。
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 の実装は、**バインディング&バリデーション&エラー設計の記事**で完全に扱います。
5. エラーハンドリング:エラーは握りつぶさず return する
Echo のエラー処理は中央集権です。ハンドラやミドルウェアは error を return するだけ。それを1つの HTTPErrorHandler が HTTP レスポンスに変換します。if err != nil { c.JSON(...) ; return } を各所に書く必要はありません。
// ステータス付きエラーを返す
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 から反転)です。
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)形式での返却や、バリデーションエラーの整形はエラー設計の記事で扱います。
6. ミドルウェア:適用レベルと順序がすべて
ミドルウェアは func(echo.HandlerFunc) echo.HandlerFunc で、リクエストの前後に処理を挟みます。適用レベルは3つ。
e.Use(middleware.Recover()) // ① 全ルートに適用(最外周)
api := e.Group("/api", middleware.CORS(...)) // ② グループ単位
e.GET("/admin", handler, requireAdmin) // ③ 個別ルート
本番で最初に入れるべき定番は次の通りです(順序が意味を持つことに注意)。
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 ミドルウェア完全ガイド で網羅します。
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) に渡すだけです。
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 の本番ガイドやAzure Container Apps の本番ガイドとも地続きです。
8. 可観測性:slog 構造化ログ + 相関 ID
「落ちたとき追えるか」は、ログを 構造化 し、相関 ID で1リクエストを貫通させられるかで決まります。v5 はロガーを標準の log/slog に統一したので、JSON ログが標準で得られます。
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)の記事で、プラットフォーム横断の設計は可観測性の実践ガイドで扱います。ファイルアップロード(S3・presigned URL)、リアルタイム(WebSocket/SSE)、API ドキュメント(OpenAPI/Swagger)も各論で深掘りしています。
9. アーキテクチャ:ハンドラを薄く保ち、DI でテスト可能にする
Echo はあくまで HTTP の入出力層です。ビジネスロジックをハンドラに書き込むと、テストが書けず、変更が怖くなります。私が飲食店マッチングサイトのバックエンドで採った構成は、Go の定石であるクリーンアーキテクチャ + DI です。
- Controller(Echo ハンドラ):HTTP の入出力だけ。Bind/Validate して UseCase を呼び、結果を JSON にする。
- UseCase:アプリケーションのユースケース。HTTP も DB も知らない純粋なロジック。
- Repository:永続化の抽象。インターフェースで定義し、実装(PostgreSQL 等)を差し替え可能にする。
- DI(
google/wire):依存の配線をコンパイル時に解決。ハンドラに具象を直接持たせない(依存性逆転)。
// 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 の記事で、Repository 実装(pgx/sqlc/GORM・トランザクション・接続プール)はデータベース層の記事で、認証・認可(JWT 発行/検証・RBAC・リフレッシュトークン)は認証・認可の記事で深掘りしています。
10. テスト:ハンドラは「Context を渡すだけ」で検証できる
Echo のハンドラは *echo.Context を受け取る素の関数なので、net/http/httptest でそのままテストできます。これが「テストしやすさ」の源泉です。
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 パッケージがあり、ボイラープレートをさらに減らせます。
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 テスト戦略の記事で体系化しています。
11. デプロイ:マルチステージ Docker で最小・安全に出す
Go の強みは単一バイナリです。マルチステージビルドで「ビルド環境」と「実行環境」を分け、実行イメージを極小・最小権限にします。
# --- 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 を受けてグレースフルに止まるコードと合わせて初めて、ローリングデプロイで無停止になります。サーバータイムアウト(v5 は BeforeServeFunc)・ヘルスチェック・ECS/Cloud Run の作法まで含めた本番デプロイは、デプロイ完全ガイドで詳説します。
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 徹底比較 で深掘りしています。
私自身は、フロントを Next.js、バックエンドを Go/Echo で組む構成を採ってきました。OpenAPI を契約に据えてクライアント⇄サーバーの型整合をビルド時に保証する設計は、Next.js × Go × OpenAPI の記事で詳述しています。
まとめ:Echo を「本番品質」に引き上げる7つの要点
- v5 の差分を最初に押さえる——
*echo.Context・slog・StartConfig。v4 サンプルは無修正では動かない。 - ルーティングは優先順位(静的→param→*)を理解し、権限境界はグループで束ねる。
- **入力は DTO に
Bind→Validate**して境界で殺す。ビジネス構造体に直接バインドしない。 - エラーは握りつぶさず
return。集中HTTPErrorHandlerで一貫した JSON とログ送出に集約する。 Recoverを最外周に置き、panic でプロセスを殺さない。StartConfig{GracefulTimeout}+SIGTERMでローリングデプロイを無停止にする。- ハンドラを薄く保ち DI でテスト可能に。Echo は HTTP 層、ロジックは UseCase へ。
Echo は「軽さ」と「本番に必要な部品」の中間にいるからこそ、設計者の判断がそのまま品質に出るフレームワークです。この記事の型を土台に、ミドルウェアとバインディング&バリデーション&エラー設計を押さえれば、落ちない・追える・変更しやすい API に届きます。