# 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 - **GitHub**: https://github.com/labstack/echo - **Official Docs**: https://echo.labstack.com/docs - **GoDoc**: https://pkg.go.dev/github.com/labstack/echo/v4 - **Examples**: https://github.com/labstack/echo/tree/master/examples --- ## Installation ```bash go get github.com/labstack/echo/v4 go get github.com/labstack/echo/v4/middleware ``` --- ## Key Concepts ### 1. Basic Server Setup ```go 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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, "

Hello

") // 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 ```go // 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 ```go // 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 ```go // 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 ```go 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 ```go 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 ```go 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 ```go // 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 ```go // 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 ```go 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 ```go 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 ```go 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 - **Official Examples**: https://github.com/labstack/echo/tree/master/examples - **Echo Cookbook**: https://echo.labstack.com/cookbook/ - **Middleware Guide**: https://echo.labstack.com/middleware/ - **Binding Guide**: https://echo.labstack.com/guide/binding/ - **Validation**: https://echo.labstack.com/guide/request/ --- ## 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.