Echo #

Echo adalah web framework Go yang memposisikan diri di titik tengah antara produktivitas developer dan performa — lebih opinionated dari net/http standar tetapi lebih dekat ke idiom Go dibandingkan Fiber. Echo dibangun di atas net/http sehingga kompatibel penuh dengan ekosistem middleware dan library Go yang sudah ada. Keunggulan utama Echo terletak pada tiga hal: sistem binding dan validasi yang fleksibel, dukungan custom context yang kuat untuk memperluas fungsionalitas tanpa global state, dan dukungan HTTP/2 serta WebSocket yang mature. Artikel ini membahas semua fitur utama Echo dari instalasi hingga pola organisasi kode yang siap production.

Instalasi #

go get github.com/labstack/echo/v4

Server minimal:

package main

import (
    "net/http"
    "github.com/labstack/echo/v4"
)

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

    e.GET("/ping", func(c echo.Context) error {
        return c.JSON(http.StatusOK, map[string]string{"message": "pong"})
    })

    e.Logger.Fatal(e.Start(":8080"))
}
Echo menggunakan net/http sebagai transport layer, sehingga semua middleware yang ditulis untuk http.Handler bisa digunakan via echo.WrapMiddleware(). Ini adalah keunggulan Echo dibandingkan Fiber yang tidak kompatibel dengan ekosistem net/http.

Cara Kerja Request di Echo #

Echo memperkenalkan konsep echo.Context yang membungkus http.Request dan http.ResponseWriter standar sambil menambahkan method-method yang ekspresif. Memahami alur request sangat penting sebelum menulis middleware.

flowchart TD
    A([HTTP Request]) --> B["net/http Server"]
    B --> C["echo.Context dibuat\n(wraps Request + ResponseWriter)"]
    C --> D["Router — radix tree matching"]
    D --> E{Route ditemukan?}
    E -- Tidak --> F["HTTPErrorHandler\n404 Not Found"]
    E -- Ya --> G["Middleware Chain\n(Pre + Group + Route level)"]
    G --> H["Handler Utama"]
    H --> I["Response ditulis\nke ResponseWriter"]
    F --> I
    I --> J([Response ke Client])

    style A fill:#3b82f6,color:#fff
    style J fill:#3b82f6,color:#fff
    style F fill:#e05252,color:#fff

Echo menggunakan radix tree untuk routing yang memberikan lookup O(log n) bahkan untuk ribuan route. Berbeda dengan Gin yang menggunakan httprouter, Echo membangun tree-nya sendiri dengan dukungan parameter yang lebih fleksibel.


Konfigurasi dan Inisialisasi #

Echo bisa dikustomisasi cukup dalam saat inisialisasi:

e := echo.New()

// Sembunyikan banner startup di production
e.HideBanner = true
e.HidePort  = true

// Custom logger
e.Logger.SetLevel(log.INFO)

// Custom HTTP error handler global
e.HTTPErrorHandler = func(err error, c echo.Context) {
    code := http.StatusInternalServerError
    msg  := "terjadi kesalahan internal"

    var he *echo.HTTPError
    if errors.As(err, &he) {
        code = he.Code
        if m, ok := he.Message.(string); ok {
            msg = m
        }
    }

    // Jangan bocorkan detail error di production
    if os.Getenv("APP_ENV") != "production" {
        msg = err.Error()
    }

    c.JSON(code, map[string]interface{}{
        "success": false,
        "error":   msg,
        "path":    c.Request().URL.Path,
    })
}

Routing #

Routing Echo menggunakan konvensi method-per-HTTP-verb yang bersih dan mudah dibaca.

e.GET("/users", listUsers)
e.POST("/users", createUser)
e.PUT("/users/:id", updateUser)
e.PATCH("/users/:id", patchUser)
e.DELETE("/users/:id", deleteUser)

// Semua method
e.Any("/webhook", handleWebhook)

// Method kustom (misalnya untuk WebDAV)
e.Add("PROPFIND", "/dav/*", davHandler)

Parameter Route #

Echo mendukung tiga jenis parameter dalam path:

// Parameter bernama — wajib ada
e.GET("/users/:id", func(c echo.Context) error {
    id := c.Param("id")
    return c.JSON(http.StatusOK, map[string]string{"id": id})
})

// Wildcard — menangkap semua segmen setelah /files/
e.GET("/files/*", func(c echo.Context) error {
    path := c.Param("*")
    return c.JSON(http.StatusOK, map[string]string{"path": path})
})

// Beberapa parameter
e.GET("/orgs/:orgID/repos/:repoID", func(c echo.Context) error {
    orgID  := c.Param("orgID")
    repoID := c.Param("repoID")
    return c.JSON(http.StatusOK, map[string]string{
        "org":  orgID,
        "repo": repoID,
    })
})

Query String #

// GET /search?q=golang&page=2&limit=20
e.GET("/search", func(c echo.Context) error {
    q     := c.QueryParam("q")
    page  := c.QueryParam("page")
    limit := c.QueryParam("limit")

    // Dengan default value
    if page == "" {
        page = "1"
    }

    return c.JSON(http.StatusOK, map[string]string{
        "q": q, "page": page, "limit": limit,
    })
})

Route Groups #

// Grup dengan prefix
api := e.Group("/api")

// v1 — publik
v1 := api.Group("/v1")
v1.GET("/status", statusHandler)

// v2 — memerlukan autentikasi
v2 := api.Group("/v2", authMiddleware)
{
    users := v2.Group("/users")
    users.GET("", listUsers)
    users.POST("", createUser)
    users.GET("/:id", getUserByID)
    users.PUT("/:id", updateUser)
    users.DELETE("/:id", deleteUser)

    // Nested group dengan middleware tambahan
    admin := v2.Group("/admin", adminOnlyMiddleware)
    admin.GET("/metrics", metricsHandler)
    admin.GET("/logs", logsHandler)
}
graph TD
    A["/api"] --> B["/v1\n(publik)"]
    A --> C["/v2\n+ authMiddleware"]
    B --> D["GET /status"]
    C --> E["/users"]
    C --> F["/admin\n+ adminOnly"]
    E --> G["GET /"]
    E --> H["POST /"]
    E --> I["GET /:id"]
    E --> J["PUT /:id"]
    E --> K["DELETE /:id"]
    F --> L["GET /metrics"]
    F --> M["GET /logs"]

Middleware #

Echo mendukung tiga level pendaftaran middleware: global (semua route), group (sekumpulan route), dan route (satu route spesifik). Ini memberi kontrol granular yang tidak dimiliki framework lain secara built-in.

Level Pendaftaran Middleware #

// 1. Global — berlaku untuk semua route
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// 2. Group — berlaku untuk semua route dalam grup
adminGroup := e.Group("/admin", adminAuth)

// 3. Route — berlaku untuk satu route saja
e.GET("/sensitive", handler, rateLimiter, auditLog)
graph LR
    subgraph "Global Middleware"
        A["Logger"] --> B["Recover"]
    end
    subgraph "Routes"
        B --> C["GET /health\n(no extra middleware)"]
        B --> D["GET /admin/...\n+ adminAuth"]
        B --> E["GET /sensitive\n+ rateLimiter + auditLog"]
    end

Middleware Bawaan Echo #

Echo menyertakan middleware berkualitas tinggi di subpackage middleware:

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

// Logger terstruktur
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
    Format: `{"time":"${time_rfc3339}","method":"${method}","uri":"${uri}","status":${status},"latency":"${latency_human}"}` + "\n",
}))

// Recovery dari panic
e.Use(middleware.Recover())

// CORS
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
    AllowOrigins: []string{"https://app.example.com"},
    AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAuthorization},
    AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
}))

// Rate limiter
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))

// Gzip compression
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
    Level: 5,
}))

// Request ID
e.Use(middleware.RequestID())

// JWT
e.Use(middleware.JWTWithConfig(middleware.JWTConfig{
    SigningKey: []byte(os.Getenv("JWT_SECRET")),
}))

// Secure headers (XSS, HSTS, dll)
e.Use(middleware.Secure())

Middleware Kustom #

func AuditMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        start := time.Now()

        // Sebelum handler
        requestID := c.Response().Header().Get(echo.HeaderXRequestID)

        err := next(c) // panggil handler berikutnya

        // Setelah handler selesai
        log.Printf("audit | id=%s method=%s path=%s status=%d latency=%v",
            requestID,
            c.Request().Method,
            c.Request().URL.Path,
            c.Response().Status,
            time.Since(start),
        )

        return err
    }
}

Urutan Eksekusi Middleware #

sequenceDiagram
    participant Client
    participant Logger as LoggerMiddleware
    participant Auth as AuthMiddleware
    participant H as Handler

    Client->>Logger: Request masuk
    Logger->>Logger: Catat waktu mulai
    Logger->>Auth: next(c)
    Auth->>Auth: Validasi JWT
    alt Token tidak valid
        Auth-->>Logger: return HTTPError 401
        Logger->>Logger: Catat status 401
        Logger-->>Client: 401 Unauthorized
    else Token valid
        Auth->>Auth: Set user ke context
        Auth->>H: next(c)
        H->>H: Proses bisnis
        H-->>Auth: return nil
        Auth-->>Logger: return nil
        Logger->>Logger: Catat status & durasi
        Logger-->>Client: 200 Response
    end

Binding dan Validasi #

Echo memiliki sistem binding yang paling fleksibel di antara tiga framework ini. Satu method Bind() menangani semua sumber data, dan validasi bisa diintegrasikan langsung ke dalam lifecycle binding.

Binding Dasar #

type CreateUserRequest struct {
    Name  string `json:"name"  form:"name"  query:"name"  validate:"required,min=2,max=100"`
    Email string `json:"email" form:"email" query:"email" validate:"required,email"`
    Age   int    `json:"age"   form:"age"   query:"age"   validate:"required,gte=18"`
}

func createUser(c echo.Context) error {
    req := new(CreateUserRequest)

    if err := c.Bind(req); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }

    if err := c.Validate(req); err != nil {
        return err // ditangani HTTPErrorHandler
    }

    return c.JSON(http.StatusCreated, map[string]interface{}{
        "success": true,
        "data":    req,
    })
}

Mendaftarkan Validator Global #

Echo tidak menyertakan implementasi validator bawaan — ia mendefinisikan interface echo.Validator yang harus kamu implementasikan:

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

type CustomValidator struct {
    validator *validator.Validate
}

func (cv *CustomValidator) Validate(i interface{}) error {
    if err := cv.validator.Struct(i); err != nil {
        return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
    }
    return nil
}

// Daftarkan sekali saat inisialisasi
func main() {
    e := echo.New()
    e.Validator = &CustomValidator{validator: validator.New()}
    // ...
}

Binding dari Sumber Spesifik #

// Hanya dari path params
type UserURI struct {
    ID uint `param:"id" validate:"required"`
}
req := new(UserURI)
if err := c.Bind(req); err != nil { ... }

// Hanya dari query string
type PaginationQuery struct {
    Page  int `query:"page"`
    Limit int `query:"limit"`
}

// Binding manual dari query dengan default
page,  _ := strconv.Atoi(c.QueryParam("page"))
limit, _ := strconv.Atoi(c.QueryParam("limit"))
if page  <= 0 { page  = 1  }
if limit <= 0 { limit = 20 }
flowchart LR
    A["JSON Body"] --> E["c.Bind(&req)"]
    B["Form Data"] --> E
    C["Query String"] --> E
    D["Path Params\n:id"] --> E
    E --> F["Struct Go\nterisi"]
    F --> G["c.Validate(&req)"]
    G --> H{CustomValidator}
    H -- Gagal --> I["422 HTTPError"]
    H -- Lolos --> J["Handler Logic"]

Custom Context #

Custom context adalah fitur Echo yang paling membedakannya dari Gin dan Fiber. Alih-alih menyimpan data di map string (c.Set/c.Get), kamu bisa memperluas echo.Context dengan field dan method yang bertipe — menghilangkan type assertion sepenuhnya.

Mendefinisikan Custom Context #

// Definisi custom context
type AppContext struct {
    echo.Context
    UserID   int
    UserRole string
    TraceID  string
}

// Middleware yang menyuntikkan custom context
func AppContextMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        traceID := c.Response().Header().Get(echo.HeaderXRequestID)

        cc := &AppContext{
            Context: c,
            TraceID: traceID,
        }

        return next(cc)
    }
}

// Middleware autentikasi yang mengisi field context
func AuthMiddlewareWithContext(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        cc := c.(*AppContext) // type assertion hanya sekali di sini

        token := c.Request().Header.Get("Authorization")
        userID, role, err := validateToken(token)
        if err != nil {
            return echo.NewHTTPError(http.StatusUnauthorized, "token tidak valid")
        }

        cc.UserID   = userID
        cc.UserRole = role

        return next(cc)
    }
}

Menggunakan Custom Context di Handler #

// ANTI-PATTERN: type assertion di setiap handler
func getProfile(c echo.Context) error {
    userID, ok := c.Get("userID").(int) // type assertion berulang
    if !ok {
        return echo.NewHTTPError(http.StatusUnauthorized)
    }
    // ...
}

// BENAR: custom context — tidak ada type assertion di handler
func getProfile(c echo.Context) error {
    cc := c.(*AppContext) // satu type assertion, tipe sudah pasti

    // Akses langsung tanpa casting
    userID   := cc.UserID
    userRole := cc.UserRole
    traceID  := cc.TraceID

    return c.JSON(http.StatusOK, map[string]interface{}{
        "userID":   userID,
        "userRole": userRole,
        "traceID":  traceID,
    })
}
flowchart TD
    A["echo.Context\n(bawaan)"] -->|"Embedding"| B["AppContext\n+ UserID int\n+ UserRole string\n+ TraceID string"]
    C["AppContextMiddleware"] -->|"Bungkus c"| B
    D["AuthMiddleware"] -->|"Isi UserID & UserRole"| B
    B --> E["Handler\ncc := c.(*AppContext)\ncc.UserID — tanpa type assertion"]

Response #

Echo menyediakan method response yang ekspresif dan konsisten karena semua handler mengembalikan error.

JSON dan Format Lain #

// JSON
return c.JSON(http.StatusOK, user)

// JSON dengan pretty print (untuk debugging)
return c.JSONPretty(http.StatusOK, user, "  ")

// JSONP (untuk cross-origin dari browser lama)
return c.JSONP(http.StatusOK, "callback", user)

// XML
return c.XML(http.StatusOK, user)

// String
return c.String(http.StatusOK, "Hello, World!")

// HTML
return c.HTML(http.StatusOK, "<h1>Hello</h1>")

// File
return c.File("/path/to/report.pdf")
return c.Attachment("/path/to/report.pdf", "laporan.pdf")

// Redirect
return c.Redirect(http.StatusMovedPermanently, "https://example.com")

// No content
return c.NoContent(http.StatusNoContent)

Response Helper Terpusat #

// pkg/response/response.go
package response

import (
    "net/http"
    "github.com/labstack/echo/v4"
)

type Response struct {
    Success bool        `json:"success"`
    Data    interface{} `json:"data,omitempty"`
    Error   string      `json:"error,omitempty"`
}

type Meta struct {
    Page       int `json:"page"`
    Limit      int `json:"limit"`
    TotalItems int `json:"total_items"`
    TotalPages int `json:"total_pages"`
}

type PaginatedResponse struct {
    Success bool        `json:"success"`
    Data    interface{} `json:"data"`
    Meta    Meta        `json:"meta"`
}

func OK(c echo.Context, data interface{}) error {
    return c.JSON(http.StatusOK, Response{Success: true, Data: data})
}

func Created(c echo.Context, data interface{}) error {
    return c.JSON(http.StatusCreated, Response{Success: true, Data: data})
}

func Paginated(c echo.Context, data interface{}, meta Meta) error {
    return c.JSON(http.StatusOK, PaginatedResponse{
        Success: true, Data: data, Meta: meta,
    })
}

func BadRequest(c echo.Context, msg string) error {
    return echo.NewHTTPError(http.StatusBadRequest, msg)
}

func NotFound(c echo.Context, resource string) error {
    return echo.NewHTTPError(http.StatusNotFound, resource+" tidak ditemukan")
}

Upload File #

func uploadFile(c echo.Context) error {
    // File tunggal
    file, err := c.FormFile("file")
    if err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, "file tidak ditemukan")
    }

    // Validasi ukuran
    if file.Size > 10*1024*1024 { // 10 MB
        return echo.NewHTTPError(http.StatusRequestEntityTooLarge,
            "ukuran file melebihi batas 10 MB")
    }

    // Validasi tipe
    ext := strings.ToLower(filepath.Ext(file.Filename))
    allowed := map[string]bool{".jpg": true, ".png": true, ".pdf": true}
    if !allowed[ext] {
        return echo.NewHTTPError(http.StatusBadRequest, "tipe file tidak diizinkan")
    }

    src, err := file.Open()
    if err != nil {
        return err
    }
    defer src.Close()

    // Simpan dengan nama unik
    filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
    dst, err := os.Create(filepath.Join("uploads", filename))
    if err != nil {
        return err
    }
    defer dst.Close()

    if _, err = io.Copy(dst, src); err != nil {
        return err
    }

    return c.JSON(http.StatusOK, map[string]interface{}{
        "filename": filename,
        "size":     file.Size,
    })
}

WebSocket #

Echo menyediakan dukungan WebSocket via package golang.org/x/net/websocket atau library pihak ketiga seperti gorilla/websocket.

go get golang.org/x/net/websocket
import "golang.org/x/net/websocket"

func chatHandler(c echo.Context) error {
    websocket.Handler(func(ws *websocket.Conn) {
        defer ws.Close()

        // Ambil data dari custom context jika perlu
        // cc := c.(*AppContext)

        for {
            var msg string
            if err := websocket.Message.Receive(ws, &msg); err != nil {
                break // client disconnect
            }

            response := fmt.Sprintf("echo: %s", msg)
            if err := websocket.Message.Send(ws, response); err != nil {
                break
            }
        }
    }).ServeHTTP(c.Response(), c.Request())

    return nil
}

e.GET("/ws/chat", chatHandler)

WebSocket dengan Gorilla (Direkomendasikan) #

go get github.com/gorilla/websocket
import "github.com/gorilla/websocket"

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        // Validasi origin di production
        return r.Header.Get("Origin") == "https://app.example.com"
    },
}

func chatHandler(c echo.Context) error {
    ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
    if err != nil {
        return err
    }
    defer ws.Close()

    for {
        msgType, msg, err := ws.ReadMessage()
        if err != nil {
            break
        }

        if err := ws.WriteMessage(msgType, msg); err != nil {
            break
        }
    }

    return nil
}
sequenceDiagram
    participant Client
    participant Echo as Echo Router
    participant WS as WebSocket Handler

    Client->>Echo: GET /ws/chat\nUpgrade: websocket
    Echo->>WS: Route ke handler
    WS->>WS: upgrader.Upgrade()
    WS->>Client: 101 Switching Protocols
    loop Koneksi aktif
        Client->>WS: ReadMessage()
        WS->>WS: Proses pesan
        WS->>Client: WriteMessage()
    end
    Client->>WS: Close frame
    WS->>WS: ws.Close()

HTTP/2 #

Echo mendukung HTTP/2 secara native melalui TLS. Tidak perlu konfigurasi tambahan — cukup jalankan server dengan HTTPS.

// HTTP/2 aktif otomatis saat menggunakan StartTLS
e.Logger.Fatal(e.StartTLS(":443", "cert.pem", "key.pem"))

// Atau dengan auto-TLS via Let's Encrypt
e.Logger.Fatal(e.StartAutoTLS(":443"))
// Untuk development dengan self-signed certificate
// generate dulu: openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
e.Logger.Fatal(e.StartTLS(":8443", "cert.pem", "key.pem"))
HTTP/2 memberikan keuntungan multiplexing (banyak request dalam satu koneksi TCP), header compression, dan server push. Untuk API yang melayani banyak resource kecil secara paralel, HTTP/2 bisa mengurangi latency secara signifikan dibandingkan HTTP/1.1.

Error Handling #

Echo menggunakan echo.HTTPError sebagai tipe error standar yang membawa status code dan pesan. Semua error yang dikembalikan handler akan ditangani HTTPErrorHandler global.

// Kembalikan error dengan status code yang tepat
return echo.NewHTTPError(http.StatusNotFound, "user tidak ditemukan")
return echo.NewHTTPError(http.StatusBadRequest, "format email tidak valid")
return echo.NewHTTPError(http.StatusForbidden, "akses ditolak")

// Error internal — jangan bocorkan detail ke client
func getUserByID(c echo.Context) error {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, "ID harus berupa angka")
    }

    user, err := userService.GetByID(id)
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            return echo.NewHTTPError(http.StatusNotFound, "user tidak ditemukan")
        }
        // Log detail error, tapi jangan kirim ke client
        c.Logger().Errorf("GetByID failed: %v", err)
        return echo.ErrInternalServerError
    }

    return c.JSON(http.StatusOK, user)
}
flowchart TD
    A["Handler return error"] --> B{echo.HTTPError?}
    B -- Ya --> C["Ambil Code & Message\ndari HTTPError"]
    B -- Tidak --> D["Code: 500\nMessage: Internal Server Error"]
    C --> E["HTTPErrorHandler\nformat JSON response"]
    D --> E
    E --> F{APP_ENV == production?}
    F -- Ya --> G["Sembunyikan detail error"]
    F -- Tidak --> H["Tampilkan detail error\nuntuk debugging"]
    G --> I([Response ke Client])
    H --> I

Graceful Shutdown #

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

    // ... setup routes dan middleware

    // Jalankan server di goroutine terpisah
    go func() {
        if err := e.Start(":8080"); err != nil && err != http.ErrServerClosed {
            e.Logger.Fatal("server error:", err)
        }
    }()

    // Tunggu sinyal interrupt
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
    <-quit

    // Graceful shutdown dengan timeout 10 detik
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    if err := e.Shutdown(ctx); err != nil {
        e.Logger.Fatal("shutdown error:", err)
    }

    e.Logger.Info("server berhenti.")
}

Struktur Project yang Direkomendasikan #

myapp/
  ├── main.go
  ├── internal/
  │   ├── handler/
  │   │   ├── user.go
  │   │   └── product.go
  │   ├── middleware/
  │   │   ├── auth.go
  │   │   ├── context.go      ← custom context didefinisikan di sini
  │   │   └── error.go
  │   ├── service/
  │   │   └── user.go
  │   └── repository/
  │       └── user.go
  ├── pkg/
  │   ├── response/
  │   │   └── response.go
  │   └── validator/
  │       └── validator.go    ← CustomValidator diimplementasikan di sini
  └── router/
      └── router.go

Setup Router #

// router/router.go
package router

import (
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
    "myapp/internal/handler"
    mw "myapp/internal/middleware"
    "myapp/pkg/validator"
)

func Setup(userHandler *handler.UserHandler) *echo.Echo {
    e := echo.New()
    e.HideBanner = true

    // Validator global
    e.Validator = validator.New()

    // Custom error handler
    e.HTTPErrorHandler = mw.ErrorHandler

    // Middleware global
    e.Use(middleware.Recover())
    e.Use(middleware.RequestID())
    e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
        Format: `{"time":"${time_rfc3339}","id":"${id}","method":"${method}","uri":"${uri}","status":${status}}` + "\n",
    }))
    e.Use(mw.AppContextMiddleware) // injeksi custom context

    // Health check
    e.GET("/health", func(c echo.Context) error {
        return c.JSON(200, map[string]string{"status": "ok"})
    })

    // API routes
    api := e.Group("/api/v1")
    api.Use(mw.AuthMiddlewareWithContext)
    {
        users := api.Group("/users")
        users.GET("", userHandler.List)
        users.POST("", userHandler.Create)
        users.GET("/:id", userHandler.GetByID)
        users.PUT("/:id", userHandler.Update)
        users.DELETE("/:id", userHandler.Delete)
    }

    return e
}

Perbandingan Echo vs Gin vs Fiber #

Setelah membahas ketiganya, berikut perbedaan utama yang membantu kamu memilih:

AspekEchoGinFiber
Transportnet/httpnet/httpFasthttp
PerformaTinggiTinggiTertinggi
Kompatibilitas net/http✓ Penuh✓ Penuh✗ Partial (adaptor)
Custom context✓ Built-in✗ (pakai c.Set/Get)✗ (pakai c.Locals)
Validator bawaanInterface saja✓ via tag binding✗ (manual)
WebSocket✓ MatureTerbatas✓ via package
HTTP/2✓ Native
Middleware levelGlobal/Group/RouteGlobal/GroupGlobal/Group/Route

Kapan Tidak Menggunakan Echo #

Tetap gunakan Echo jika:
  ✓ Butuh custom context yang type-safe tanpa boilerplate
  ✓ HTTP/2 adalah kebutuhan (API yang melayani banyak resource paralel)
  ✓ Butuh kompatibilitas penuh dengan ekosistem net/http
  ✓ Tim menginginkan framework yang dekat dengan idiom Go

Pertimbangkan Gin jika:
  ✗ Tim sudah familier dengan Gin dan tidak butuh custom context
  ✗ Binding dengan validasi terintegrasi (tag binding) lebih diprioritaskan

Pertimbangkan Fiber jika:
  ✗ Performa raw adalah prioritas absolut
  ✗ Background tim adalah Express.js / Node.js

Pertimbangkan net/http standar jika:
  ✗ Aplikasi sangat sederhana dan tidak butuh abstraksi framework

Ringkasan #

  • Custom context — keunggulan utama Echo; extend echo.Context dengan field bertipe untuk menghilangkan type assertion di setiap handler.
  • c.Bind() universal — satu method menangani JSON, XML, form, query, dan path param berdasarkan Content-Type dan tag struct.
  • Validator via interface — implementasikan echo.Validator dengan go-playground/validator; daftarkan sekali di e.Validator dan panggil c.Validate() di handler.
  • Tiga level middleware — global (e.Use), group (g.Use), dan per-route (e.GET("/path", handler, mw1, mw2)); gunakan ini untuk kontrol granular.
  • echo.NewHTTPError — kembalikan error dengan status code yang tepat; biarkan HTTPErrorHandler global yang memformatnya secara konsisten.
  • HTTP/2 native — aktif otomatis saat menggunakan StartTLS atau StartAutoTLS; tidak perlu konfigurasi tambahan.
  • Kompatibel net/http — semua middleware http.Handler bisa digunakan via echo.WrapMiddleware(); tidak seperti Fiber yang butuh adapter khusus.
  • Graceful shutdown — gunakan e.Shutdown(ctx) dengan context ber-timeout agar request yang sedang berjalan selesai sebelum server mati.

← Sebelumnya: Fiber   Berikutnya: Revel →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact