Echo

High-performance and minimal Go web framework. Provides more built-in functionality, suitable for larger applications.

GoFrameworkWeb DevelopmentREST APIMinimalistHigh Performance

GitHub Overview

labstack/echo

High performance, minimalist Go web framework

Stars31,267
Watchers530
Forks2,276
Created:March 1, 2015
Language:Go
License:MIT License

Topics

echogohttp2httpslabstack-echoletsencryptmicro-frameworkmicroservicemiddlewaresslwebweb-frameworkwebsocket

Star History

labstack/echo Star History
Data as of: 7/16/2025, 08:43 AM

Framework

Echo

Overview

Echo is a high-performance, minimalist web framework written in Go. It specializes in building RESTful APIs and web applications.

Details

Echo was developed by LabStack in 2015, designed as a Go web framework that emphasizes simplicity and performance. It features an optimized HTTP router, rich middleware ecosystem, and intuitive API that enables developers to rapidly build high-performance web applications. Echo uses a tree-based routing algorithm for efficient route matching. The middleware system is hierarchical, allowing application at global, group, and route levels. It includes automatic binding for JSON, XML, and form data, centralized error handling, HTTP/2 support, and automatic TLS with Let's Encrypt. Despite its lightweight nature, it provides enterprise-level features, making it ideal for microservice architectures and API-first development approaches.

Pros and Cons

Pros

  • High Performance: Optimized router and lightweight design for fast response times
  • Simple Learning Curve: Intuitive API with clear documentation
  • Rich Middleware: Built-in logging, authentication, CORS, compression, and more
  • Flexible Routing: Support for parameters, wildcards, and route grouping
  • Automatic Binding: Automatic conversion of JSON and form data
  • HTTP/2 Support: Modern HTTP protocol support
  • Active Community: Active development and community support

Cons

  • Limited Features: More limited functionality compared to full-stack frameworks
  • Go Language Dependency: Go-specific learning curve and development environment constraints
  • No Built-in ORM: Requires separate libraries for database operations
  • Template Limitations: Basic built-in template engine functionality
  • Ecosystem: Relatively fewer framework-specific tools

Key Links

Code Examples

Hello World

package main

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

func main() {
	// Create Echo instance
	e := echo.New()

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

	// Routes
	e.GET("/", hello)

	// Start server
	e.Logger.Fatal(e.Start(":1323"))
}

// Handler function
func hello(c echo.Context) error {
	return c.String(http.StatusOK, "Hello, World!")
}

Routing and Parameters

package main

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

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

	// Basic routes
	e.GET("/", home)
	e.POST("/users", createUser)
	e.GET("/users/:id", getUser)
	e.PUT("/users/:id", updateUser)
	e.DELETE("/users/:id", deleteUser)

	// Wildcard route
	e.GET("/files/*", getFile)

	// Query parameters
	e.GET("/search", search)

	e.Logger.Fatal(e.Start(":1323"))
}

func home(c echo.Context) error {
	return c.JSON(http.StatusOK, map[string]string{
		"message": "Welcome to Echo API",
	})
}

func getUser(c echo.Context) error {
	// Get path parameter
	id := c.Param("id")
	return c.JSON(http.StatusOK, map[string]string{
		"id": id,
		"name": "User " + id,
	})
}

func search(c echo.Context) error {
	// Get query parameters
	query := c.QueryParam("q")
	page := c.QueryParam("page")
	
	if query == "" {
		return c.JSON(http.StatusBadRequest, map[string]string{
			"error": "Search query is required",
		})
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"query": query,
		"page": page,
		"results": []string{"Result 1", "Result 2", "Result 3"},
	})
}

func getFile(c echo.Context) error {
	// Get wildcard parameter
	filepath := c.Param("*")
	return c.JSON(http.StatusOK, map[string]string{
		"filepath": filepath,
	})
}

JSON Data Binding and Validation

package main

import (
	"net/http"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/go-playground/validator/v10"
)

type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name" validate:"required,min=2,max=50"`
	Email string `json:"email" validate:"required,email"`
	Age   int    `json:"age" validate:"min=0,max=120"`
}

type CustomValidator struct {
	validator *validator.Validate
}

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

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

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

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

	e.POST("/users", createUser)
	e.GET("/users/:id", getUser)

	e.Logger.Fatal(e.Start(":1323"))
}

func createUser(c echo.Context) error {
	user := new(User)

	// Bind JSON data
	if err := c.Bind(user); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{
			"error": "Invalid JSON format",
		})
	}

	// Validate
	if err := c.Validate(user); err != nil {
		return c.JSON(http.StatusBadRequest, map[string]string{
			"error": err.Error(),
		})
	}

	// User creation logic (actual DB save, etc.)
	user.ID = 123 // Mock ID

	return c.JSON(http.StatusCreated, user)
}

func getUser(c echo.Context) error {
	// Mock user data
	user := User{
		ID:    1,
		Name:  "John Doe",
		Email: "[email protected]",
		Age:   30,
	}

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

Using Middleware

package main

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

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

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

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

	// Security headers
	e.Use(middleware.Secure())

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

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

	// Public routes
	e.GET("/", publicHandler)
	e.POST("/register", registerHandler)

	// API group requiring authentication
	api := e.Group("/api")
	api.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
		if username == "admin" && password == "secret" {
			return true, nil
		}
		return false, nil
	}))

	api.GET("/users", listUsers)
	api.POST("/users", createUser)

	// Custom middleware
	e.Use(customLogger)

	e.Logger.Fatal(e.Start(":1323"))
}

func customLogger(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		start := time.Now()
		
		// Execute next handler
		err := next(c)
		
		// Log output
		c.Logger().Infof("Method: %s, URI: %s, Status: %d, Latency: %v",
			c.Request().Method,
			c.Request().RequestURI,
			c.Response().Status,
			time.Since(start),
		)
		
		return err
	}
}

func publicHandler(c echo.Context) error {
	return c.JSON(http.StatusOK, map[string]string{
		"message": "Public endpoint",
	})
}

func registerHandler(c echo.Context) error {
	return c.JSON(http.StatusOK, map[string]string{
		"message": "User registration",
	})
}

func listUsers(c echo.Context) error {
	return c.JSON(http.StatusOK, []map[string]interface{}{
		{"id": 1, "name": "User 1"},
		{"id": 2, "name": "User 2"},
	})
}

Error Handling

package main

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

type HTTPError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

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

	// Custom error handler
	e.HTTPErrorHandler = customHTTPErrorHandler

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

	e.GET("/", successHandler)
	e.GET("/error", errorHandler)
	e.GET("/panic", panicHandler)
	e.GET("/users/:id", getUserWithError)

	e.Logger.Fatal(e.Start(":1323"))
}

func customHTTPErrorHandler(err error, c echo.Context) {
	var code int
	var message string

	if he, ok := err.(*echo.HTTPError); ok {
		code = he.Code
		message = he.Message.(string)
	} else {
		code = http.StatusInternalServerError
		message = "Internal Server Error"
	}

	// Log error
	c.Logger().Error(err)

	// JSON error response
	if !c.Response().Committed {
		if c.Request().Method == http.MethodHead {
			err = c.NoContent(code)
		} else {
			err = c.JSON(code, HTTPError{
				Code:    code,
				Message: message,
			})
		}
		if err != nil {
			c.Logger().Error(err)
		}
	}
}

func successHandler(c echo.Context) error {
	return c.JSON(http.StatusOK, map[string]string{
		"message": "Success",
	})
}

func errorHandler(c echo.Context) error {
	return echo.NewHTTPError(http.StatusBadRequest, "Custom error message")
}

func panicHandler(c echo.Context) error {
	panic("A panic occurred")
}

func getUserWithError(c echo.Context) error {
	id := c.Param("id")
	
	// Business logic error
	if id == "999" {
		return echo.NewHTTPError(http.StatusNotFound, "User not found")
	}

	// System error
	if id == "500" {
		return errors.New("Database connection error")
	}

	return c.JSON(http.StatusOK, map[string]string{
		"id": id,
		"name": "User " + id,
	})
}

File Upload and Static File Serving

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

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

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

	// Static file serving
	e.Static("/static", "assets")

	// File upload
	e.POST("/upload", uploadFile)
	e.POST("/upload/multiple", uploadMultipleFiles)

	// File download
	e.GET("/download/:filename", downloadFile)

	e.Logger.Fatal(e.Start(":1323"))
}

func uploadFile(c echo.Context) error {
	// Get file from form
	file, err := c.FormFile("file")
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Failed to get file")
	}

	// File size check (10MB limit)
	if file.Size > 10<<20 {
		return echo.NewHTTPError(http.StatusBadRequest, "File too large (max 10MB)")
	}

	// Open file
	src, err := file.Open()
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file")
	}
	defer src.Close()

	// Create upload directory if it doesn't exist
	uploadDir := "uploads"
	if _, err := os.Stat(uploadDir); os.IsNotExist(err) {
		err = os.Mkdir(uploadDir, 0755)
		if err != nil {
			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create upload directory")
		}
	}

	// Create destination file
	dst, err := os.Create(filepath.Join(uploadDir, file.Filename))
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create file")
	}
	defer dst.Close()

	// Copy file
	if _, err = io.Copy(dst, src); err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failed to save file")
	}

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

func uploadMultipleFiles(c echo.Context) error {
	// Parse multipart form
	form, err := c.MultipartForm()
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "Failed to parse multipart form")
	}

	files := form.File["files"]
	var uploadedFiles []string

	for _, file := range files {
		// File size check
		if file.Size > 10<<20 {
			continue // Skip files that are too large
		}

		src, err := file.Open()
		if err != nil {
			continue
		}

		dst, err := os.Create(filepath.Join("uploads", file.Filename))
		if err != nil {
			src.Close()
			continue
		}

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

		src.Close()
		dst.Close()
		uploadedFiles = append(uploadedFiles, file.Filename)
	}

	return c.JSON(http.StatusOK, map[string]interface{}{
		"message":        "File upload completed",
		"uploaded_files": uploadedFiles,
		"count":          len(uploadedFiles),
	})
}

func downloadFile(c echo.Context) error {
	filename := c.Param("filename")
	filePath := filepath.Join("uploads", filename)

	// Check file existence
	if _, err := os.Stat(filePath); os.IsNotExist(err) {
		return echo.NewHTTPError(http.StatusNotFound, "File not found")
	}

	// Set Content-Disposition header to prompt download
	c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))

	return c.File(filePath)
}