echo-patterns

Echo (Go) production patterns — typed context, route groups, middleware chain, binding/validation, custom HTTPErrorHandler, graceful shutdown, and testing. Use when building or reviewing Echo services. Compare with gin-patterns for framework choice.

Echo Patterns

Production patterns for Echo — minimalist Go HTTP framework with typed context.

When to activate

  • Creating Echo services from scratch
  • Configuring middleware chain (logger, recover, CORS, rate limit)
  • Binding and validating request bodies
  • Mapping errors to HTTP status via custom HTTPErrorHandler
  • Choosing between Echo and Gin for a new project

Echo vs Gin — quick

AspectoEchoGin
Contextecho.Context (interface)*gin.Context (struct pointer)
Middleware chainExplicit Use() orderingExplicit Use() ordering
Binding tagsvalidate:"..." (custom validator)binding:"..." (go-playground/validator)
Error handlerCustomizable via HTTPErrorHandlerMiddleware pattern (c.Error + mid)
Performance~equivalent~equivalent

Choice: Echo if you prefer context as an interface and a centralized HTTPErrorHandler; Gin if you're already used to it or want a larger community.

Recommended structure

cmd/server/main.go
internal/
├── handler/
│   ├── users.go
│   └── health.go
├── middleware/
├── model/
├── repository/
└── service/

Router + Groups

func NewServer(svc *service.Service) *echo.Echo {
    e := echo.New()
    e.HideBanner = true
    e.HTTPErrorHandler = customHTTPErrorHandler
    e.Validator = &CustomValidator{v: validator.New()}

    e.Use(middleware.Recover())
    e.Use(middleware.RequestID())
    e.Use(middleware.Logger())
    e.Use(middleware.CORS())

    e.GET("/health", handler.Health)

    v1 := e.Group("/api/v1")
    v1.POST("/auth/login", handler.Login(svc))

    authed := v1.Group("", AuthMiddleware(svc))
    authed.GET("/me", handler.Me(svc))
    authed.GET("/users/:id", handler.GetUser(svc))
    authed.POST("/users", handler.CreateUser(svc))

    return e
}

Typed Context + Binding

type CreateUserRequest struct {
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=12"`
    FullName string `json:"full_name" validate:"omitempty,max=120"`
}

func CreateUser(svc *service.Service) echo.HandlerFunc {
    return func(c echo.Context) error {
        var req CreateUserRequest
        if err := c.Bind(&req); err != nil {
            return echo.NewHTTPError(http.StatusBadRequest, "invalid json")
        }
        if err := c.Validate(&req); err != nil {
            return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
        }

        user, err := svc.CreateUser(c.Request().Context(), req.Email, req.Password, req.FullName)
        if err != nil {
            return err  // HTTPErrorHandler handles the mapping
        }
        return c.JSON(http.StatusCreated, toUserResponse(user))
    }
}

// Custom validator
type CustomValidator struct{ v *validator.Validate }

func (cv *CustomValidator) Validate(i interface{}) error {
    return cv.v.Struct(i)
}

Custom HTTPErrorHandler

Instead of mapping errors in each handler (as in Gin), centralize them:

func customHTTPErrorHandler(err error, c echo.Context) {
    if c.Response().Committed {
        return
    }

    code := http.StatusInternalServerError
    errCode := "internal"
    message := "internal error"

    switch {
    case errors.Is(err, domain.ErrNotFound):
        code, errCode, message = http.StatusNotFound, "not_found", err.Error()
    case errors.Is(err, domain.ErrUnauthorized):
        code, errCode, message = http.StatusUnauthorized, "unauthorized", "unauthorized"
    case errors.Is(err, domain.ErrConflict):
        code, errCode, message = http.StatusConflict, "conflict", err.Error()
    default:
        var he *echo.HTTPError
        if errors.As(err, &he) {
            code = he.Code
            message = fmt.Sprint(he.Message)
            errCode = strings.ToLower(http.StatusText(code))
        } else {
            c.Logger().Error(err)
        }
    }

    _ = c.JSON(code, echo.Map{"error": echo.Map{"code": errCode, "message": message}})
}

Advantage: handlers stay clean — they just return err, and the mapping is a single responsibility.

Custom middleware

func AuthMiddleware(svc *service.Service) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            token := extractBearer(c.Request().Header.Get("Authorization"))
            if token == "" {
                return echo.NewHTTPError(http.StatusUnauthorized)
            }
            user, err := svc.ValidateToken(c.Request().Context(), token)
            if err != nil {
                return echo.NewHTTPError(http.StatusUnauthorized)
            }
            c.Set("user", user)
            return next(c)
        }
    }
}

Usage: user := c.Get("user").(*domain.User) — or a type-safe helper function.

Graceful Shutdown

func main() {
    e := NewServer(svc)

    go func() {
        if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) {
            e.Logger.Fatal("shutting down unexpectedly: ", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    if err := e.Shutdown(ctx); err != nil {
        e.Logger.Fatal(err)
    }
}

Testing

func TestCreateUser(t *testing.T) {
    e := echo.New()
    e.Validator = &CustomValidator{v: validator.New()}

    body := `{"email":"[email protected]","password":"secret1234567","full_name":"A B"}`
    req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
    req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
    rec := httptest.NewRecorder()
    c := e.NewContext(req, rec)

    svc := service.NewMock()
    if assert.NoError(t, handler.CreateUser(svc)(c)) {
        assert.Equal(t, http.StatusCreated, rec.Code)
    }
}

Related to

  • skill golang-patterns — Go idioms
  • skill golang-testing — table-driven tests
  • skill gin-patterns — alternative framework
  • skill api-design — REST conventions