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<br/>(wraps Request + ResponseWriter)"]
    C --> D["Router — radix tree matching"]
    D --> E{Route ditemukan?}
    E -- Tidak --> F["HTTPErrorHandler<br/>404 Not Found"]
    E -- Ya --> G["Middleware Chain<br/>(Pre + Group + Route level)"]
    G --> H["Handler Utama"]
    H --> I["Response ditulis<br/>ke ResponseWriter"]
    F --> I
    I --> J([Response ke Client])

Metode Parsing Input pada Context #

Untuk membaca input dari request, Echo menyediakan beberapa method parser pada echo.Context:

Metode Parsing Sumber Data Format / Tipe Hasil Penanganan Tipe Kasus Penggunaan Utama
c.Param("id") URL Path Parameter (misal /users/:id) string Konversi manual Mengambil parameter identitas di URL
c.QueryParam("q") Query String parameter (misal ?q=golang) string Konversi manual Filter, pencarian, dan pagination
c.FormValue("name") URL-encoded / Multipart Form Body string Konversi manual Input formulir HTML standar
c.Bind(&struct) JSON, XML, Form, Query error Otomatis via struct tags Payload JSON API atau request kompleks

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:

Aspek Echo Gin Fiber
Transport net/http net/http Fasthttp
Performa Tinggi Tinggi Tertinggi
Kompatibilitas net/http ✓ Penuh ✓ Penuh ✗ Partial (adaptor)
Custom context ✓ Built-in ✗ (pakai c.Set/Get) ✗ (pakai c.Locals)
Validator bawaan Interface saja ✓ via tag binding ✗ (manual)
WebSocket ✓ Mature Terbatas ✓ via package
HTTP/2 ✓ Native
Middleware level Global/Group/Route Global/Group Global/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