turash/dev_guides/04_echo_framework.md
Damir Mukimov 4a2fda96cd
Initial commit: Repository setup with .gitignore, golangci-lint v2.6.0, and code quality checks
- Initialize git repository
- Add comprehensive .gitignore for Go projects
- Install golangci-lint v2.6.0 (latest v2) globally
- Configure .golangci.yml with appropriate linters and formatters
- Fix all formatting issues (gofmt)
- Fix all errcheck issues (unchecked errors)
- Adjust complexity threshold for validation functions
- All checks passing: build, test, vet, lint
2025-11-01 07:36:22 +01:00

14 KiB

Echo Framework Development Guide

Library: github.com/labstack/echo/v4
Used In: MVP - Alternative HTTP framework to Gin
Purpose: High-performance, extensible web framework


Where It's Used

  • Alternative to Gin if cleaner API needed
  • HTTP API server
  • Business registration, resource flow CRUD
  • Match retrieval endpoints
  • Authentication middleware

Official Documentation


Installation

go get github.com/labstack/echo/v4
go get github.com/labstack/echo/v4/middleware

Key Concepts

1. Basic Server Setup

package main

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

func main() {
    e := echo.New()
    
    // Middleware
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
    
    // Routes
    e.GET("/health", healthHandler)
    
    // Start server
    e.Logger.Fatal(e.Start(":8080"))
}

2. Routes and Handlers

// GET request
e.GET("/api/businesses/:id", getBusiness)

// POST request
e.POST("/api/businesses", createBusiness)

// PUT request
e.PUT("/api/businesses/:id", updateBusiness)

// DELETE request
e.DELETE("/api/businesses/:id", deleteBusiness)

// Handler function
func getBusiness(c echo.Context) error {
    id := c.Param("id") // Get path parameter
    
    business, err := service.GetBusiness(id)
    if err != nil {
        return c.JSON(404, map[string]string{"error": "Not found"})
    }
    
    return c.JSON(200, business)
}

3. Request Binding

// JSON binding
type BusinessRequest struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
}

func createBusiness(c echo.Context) error {
    var req BusinessRequest
    
    // Bind JSON
    if err := c.Bind(&req); err != nil {
        return c.JSON(400, map[string]string{"error": err.Error()})
    }
    
    // Validate
    if err := c.Validate(&req); err != nil {
        return c.JSON(400, map[string]string{"error": err.Error()})
    }
    
    business, err := service.CreateBusiness(req)
    if err != nil {
        return c.JSON(500, map[string]string{"error": err.Error()})
    }
    
    return c.JSON(201, business)
}

// Query parameters
func searchBusinesses(c echo.Context) error {
    name := c.QueryParam("name")
    page := c.QueryParamDefault("page", "1")
    // ... process query
    return c.JSON(200, results)
}

// Path parameters
func getBusiness(c echo.Context) error {
    id := c.Param("id") // /businesses/:id
    // ...
}

// Form data
func createBusinessForm(c echo.Context) error {
    name := c.FormValue("name")
    email := c.FormValue("email")
    // ...
}

4. Middleware

// Global middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(corsMiddleware())

// Route-specific middleware
e.GET("/protected", protectedHandler, authMiddleware())

// Custom middleware
func authMiddleware() echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            token := c.Request().Header.Get("Authorization")
            if token == "" {
                return c.JSON(401, map[string]string{"error": "Unauthorized"})
            }
            
            // Validate token
            userID, err := validateToken(token)
            if err != nil {
                return c.JSON(401, map[string]string{"error": "Invalid token"})
            }
            
            // Store in context
            c.Set("userID", userID)
            
            return next(c)
        }
    }
}

// Built-in middleware
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
    Format: "method=${method}, uri=${uri}, status=${status}\n",
}))

5. Response Handling

// JSON response
return c.JSON(200, business)

// JSON with status code
return c.JSONPretty(200, business, "  ") // Pretty print

// String response
return c.String(200, "Success")

// HTML response
return c.HTML(200, "<h1>Hello</h1>")

// Redirect
return c.Redirect(302, "/api/v1/businesses")

// Stream
return c.Stream(200, "application/json", reader)

// File download
return c.Attachment("file.pdf", "document.pdf")

6. Error Handling

// Custom HTTP error
func getBusiness(c echo.Context) error {
    business, err := service.GetBusiness(c.Param("id"))
    if err != nil {
        if err == ErrNotFound {
            return echo.NewHTTPError(404, "Business not found")
        }
        return echo.NewHTTPError(500, "Internal server error")
    }
    return c.JSON(200, business)
}

// Error handler
func customHTTPErrorHandler(err error, c echo.Context) {
    code := http.StatusInternalServerError
    message := "Internal Server Error"
    
    if he, ok := err.(*echo.HTTPError); ok {
        code = he.Code
        message = he.Message.(string)
    }
    
    c.JSON(code, map[string]interface{}{
        "error": message,
        "code":  code,
    })
}

e.HTTPErrorHandler = customHTTPErrorHandler

7. Grouping Routes

// Route groups
api := e.Group("/api")
{
    api.GET("/businesses", listBusinesses)
    api.POST("/businesses", createBusiness)
    
    // Nested groups
    v1 := api.Group("/v1")
    {
        v1.GET("/businesses", listBusinessesV1)
    }
    
    v2 := api.Group("/v2")
    {
        v2.GET("/businesses", listBusinessesV2)
    }
}

// With middleware
authenticated := e.Group("/api")
authenticated.Use(authMiddleware())
{
    authenticated.GET("/profile", getProfile)
    authenticated.POST("/resources", createResource)
}

8. Context Usage

// Get values from context (set by middleware)
userID := c.Get("userID").(string)

// Set value in context
c.Set("businessID", businessID)

// Get request context
ctx := c.Request().Context()

// Create request with context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req := c.Request().WithContext(ctx)
c.SetRequest(req)

9. File Upload

func uploadFile(c echo.Context) error {
    // Single file
    file, err := c.FormFile("file")
    if err != nil {
        return echo.NewHTTPError(400, err.Error())
    }
    
    // Open file
    src, err := file.Open()
    if err != nil {
        return err
    }
    defer src.Close()
    
    // Save file
    dst, err := os.Create("/uploads/" + file.Filename)
    if err != nil {
        return err
    }
    defer dst.Close()
    
    if _, err = io.Copy(dst, src); err != nil {
        return err
    }
    
    return c.JSON(200, map[string]string{"message": "File uploaded"})
}

10. Validator Integration

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

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

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

// Setup
e.Validator = &CustomValidator{validator: validator.New()}

// Use in handler
type BusinessRequest struct {
    Name  string `validate:"required,min=3"`
    Email string `validate:"required,email"`
}

func createBusiness(c echo.Context) error {
    var req BusinessRequest
    if err := c.Bind(&req); err != nil {
        return err
    }
    
    if err := c.Validate(&req); err != nil {
        return echo.NewHTTPError(400, err.Error())
    }
    
    // ... process request
}

MVP-Specific Patterns

Resource Flow Handler Example

type ResourceFlowHandler struct {
    service *ResourceFlowService
}

func (h *ResourceFlowHandler) Create(c echo.Context) error {
    var req CreateResourceFlowRequest
    if err := c.Bind(&req); err != nil {
        return echo.NewHTTPError(400, err.Error())
    }
    
    // Validate business belongs to user
    userID := c.Get("userID").(string)
    if !h.service.ValidateOwnership(req.BusinessID, userID) {
        return echo.NewHTTPError(403, "Forbidden")
    }
    
    flow, err := h.service.Create(c.Request().Context(), req)
    if err != nil {
        return echo.NewHTTPError(500, err.Error())
    }
    
    return c.JSON(201, flow)
}

func (h *ResourceFlowHandler) FindMatches(c echo.Context) error {
    flowID := c.Param("id")
    
    matches, err := h.service.FindMatches(c.Request().Context(), flowID)
    if err != nil {
        return echo.NewHTTPError(500, err.Error())
    }
    
    return c.JSON(200, map[string]interface{}{
        "matches": matches,
    })
}

// Register routes
func setupRoutes(e *echo.Echo, handlers *Handlers) {
    api := e.Group("/api/v1")
    
    resources := api.Group("/resource-flows")
    resources.Use(authMiddleware())
    {
        resources.POST("", handlers.ResourceFlow.Create)
        resources.GET("/:id/matches", handlers.ResourceFlow.FindMatches)
    }
}

Middleware for Our MVP

// CORS middleware
func corsMiddleware() echo.MiddlewareFunc {
    return middleware.CORSWithConfig(middleware.CORSConfig{
        AllowOrigins: []string{"*"},
        AllowMethods: []string{echo.GET, echo.POST, echo.PUT, echo.DELETE, echo.OPTIONS},
        AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAuthorization},
    })
}

// Rate limiting
func rateLimitMiddleware() echo.MiddlewareFunc {
    // Use token bucket or sliding window
    store := memory.NewStore()
    limiter := middleware.NewRateLimiterMemoryStore(store, 100, time.Minute)
    
    return middleware.RateLimiter(limiter)
}

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

// Body limit
e.Use(middleware.BodyLimit("2M"))

Performance Tips

  1. Use middleware selectively - don't use unnecessary middleware
  2. Enable gzip - use middleware.Gzip()
  3. Use context for cancellation - c.Request().Context() for timeouts
  4. Cache static files - use middleware.Static()
  5. Reuse echo instance - don't create new instance per request

Built-in Middleware

// Logger
e.Use(middleware.Logger())

// Recover (panic recovery)
e.Use(middleware.Recover())

// CORS
e.Use(middleware.CORS())

// Gzip compression
e.Use(middleware.Gzip())

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

// Body limit
e.Use(middleware.BodyLimit("2M"))

// Timeout
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
    Timeout: 10 * time.Second,
}))

// Rate limiting
e.Use(middleware.RateLimiter(limiter))

// Static files
e.Use(middleware.Static("/static"))

// Basic auth
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
    // Validate credentials
    return username == "admin" && password == "secret", nil
}))

WebSocket Support

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

// WebSocket route
e.GET("/ws", func(c echo.Context) error {
    return c.Echo().Upgrade(c.Response().Writer, c.Request())
})

// WebSocket handler
e.GET("/ws", func(c echo.Context) error {
    ws, err := c.WebSocket()
    if err != nil {
        return err
    }
    defer ws.Close()
    
    for {
        // Read message
        msg := new(Message)
        if err := ws.ReadJSON(msg); err != nil {
            return err
        }
        
        // Process message
        response := processMessage(msg)
        
        // Send response
        if err := ws.WriteJSON(response); err != nil {
            return err
        }
    }
})

Graceful Shutdown

func main() {
    e := echo.New()
    // ... setup routes ...
    
    // Start server in goroutine
    go func() {
        if err := e.Start(":8080"); err != nil && err != http.ErrServerClosed {
            e.Logger.Fatal("shutting down the server")
        }
    }()
    
    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt)
    <-quit
    
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    
    if err := e.Shutdown(ctx); err != nil {
        e.Logger.Fatal(err)
    }
}

Testing

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

func TestGetBusiness(t *testing.T) {
    e := echo.New()
    req := httptest.NewRequest(http.MethodGet, "/api/businesses/123", nil)
    rec := httptest.NewRecorder()
    c := e.NewContext(req, rec)
    
    // Setup handler
    handler := &BusinessHandler{service: mockService}
    
    // Execute
    if assert.NoError(t, handler.Get(c)) {
        assert.Equal(t, http.StatusOK, rec.Code)
        assert.Contains(t, rec.Body.String(), "business")
    }
}

Tutorials & Resources


Best Practices

  1. Use dependency injection - inject services into handlers
  2. Handle errors consistently - use custom error handler
  3. Validate early - use validator middleware
  4. Use middleware for cross-cutting concerns - auth, logging, CORS
  5. Version your API - use route groups
  6. Context for cancellation - use c.Request().Context() for timeouts
  7. Graceful shutdown - implement proper shutdown handling

Comparison: Echo vs Gin

Feature Echo Gin
API Style More structured More flexible
Middleware Explicit Functional
Error Handling HTTP errors Error handler
Validation Built-in support Requires addon
Context Request context Custom context
Performance High Very High
Learning Curve Medium Low

Recommendation for MVP: Both work well, choose based on team preference. Echo has better validation integration, Gin is simpler.