Skip to main content

Golang Echo Framework

Overview

Echo is a high-performance, extensible, minimalist Go web framework. It provides a robust set of features for building web applications and APIs with excellent performance and developer experience.

Key Features:

  • High Performance: Optimized for speed and low memory usage
  • Extensible: Rich middleware ecosystem and plugin system
  • Validation: Built-in request validation with custom validators
  • Grouping: Route grouping for better organization
  • Binding: Automatic request/response binding
  • Rendering: Template rendering and static file serving

Getting Started

Basic Setup

Install Echo and create a simple server.

package main

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

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

e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, Echo!")
})

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

Benefits: Clean, minimal setup with excellent performance out of the box.

Project Structure

Organize your Echo application with a clear structure.

myapp/
├── main.go
├── handlers/
│ ├── user.go
│ └── product.go
├── middleware/
│ ├── auth.go
│ └── logging.go
├── models/
│ ├── user.go
│ └── product.go
├── routes/
│ └── routes.go
└── config/
└── config.go

Basic Routing

HTTP Methods

Handle different HTTP methods with Echo's intuitive routing.

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

// GET request
e.GET("/users", getUsers)

// POST request
e.POST("/users", createUser)

// PUT request
e.PUT("/users/:id", updateUser)

// DELETE request
e.DELETE("/users/:id", deleteUser)

// PATCH request
e.PATCH("/users/:id", patchUser)

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

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

func createUser(c echo.Context) error {
return c.JSON(http.StatusCreated, map[string]string{"message": "User created"})
}

func updateUser(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{"message": "Update user " + id})
}

func deleteUser(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{"message": "Delete user " + id})
}

func patchUser(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{"message": "Patch user " + id})
}

URL Parameters

Extract parameters from URLs for dynamic routing.

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

// Single parameter
e.GET("/users/:id", getUser)

// Multiple parameters
e.GET("/users/:id/posts/:postId", getUserPost)

// Optional parameters (using wildcard)
e.GET("/files/*", getFile)

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

func getUser(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{
"user_id": id,
"message": "Get user details",
})
}

func getUserPost(c echo.Context) error {
userID := c.Param("id")
postID := c.Param("postId")
return c.JSON(http.StatusOK, map[string]string{
"user_id": userID,
"post_id": postID,
"message": "Get user post",
})
}

func getFile(c echo.Context) error {
path := c.Param("*")
return c.JSON(http.StatusOK, map[string]string{
"file_path": path,
"message": "Get file",
})
}

Query Parameters

Handle query string parameters for filtering and pagination.

func getUsers(c echo.Context) error {
// Get single query parameter
page := c.QueryParam("page")

// Get query parameter with default value
limit := c.QueryParam("limit")
if limit == "" {
limit = "10"
}

// Get all query parameters
queryParams := c.QueryParams()

return c.JSON(http.StatusOK, map[string]interface{}{
"page": page,
"limit": limit,
"all_params": queryParams,
"message": "Users list",
})
}

Request Handling

Request Binding

Bind incoming requests to Go structs automatically.

type User struct {
ID int `json:"id"`
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=130"`
Password string `json:"password" validate:"required,min=6"`
}

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

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

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

// Process the user...
user.ID = 1 // Simulate database insertion

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

Form Data

Handle form submissions and file uploads.

type UserForm struct {
Name string `form:"name"`
Email string `form:"email"`
Password string `form:"password"`
}

func handleForm(c echo.Context) error {
user := new(UserForm)

// Bind form data
if err := c.Bind(user); err != nil {
return err
}

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

func uploadFile(c echo.Context) error {
// Get uploaded file
file, err := c.FormFile("file")
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "No file uploaded",
})
}

// Validate file size (10MB limit)
if file.Size > 10*1024*1024 {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "File too large",
})
}

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

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(http.StatusOK, map[string]string{
"message": "File uploaded successfully",
"filename": file.Filename,
})
}

Response Handling

JSON Responses

Return structured JSON responses with proper status codes.

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

func getUsers(c echo.Context) error {
users := []User{
{ID: 1, Name: "John", Email: "john@example.com"},
{ID: 2, Name: "Jane", Email: "jane@example.com"},
}

response := Response{
Success: true,
Message: "Users retrieved successfully",
Data: users,
}

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

func getUser(c echo.Context) error {
id := c.Param("id")

// Simulate user not found
if id == "999" {
response := Response{
Success: false,
Error: "User not found",
}
return c.JSON(http.StatusNotFound, response)
}

user := User{ID: 1, Name: "John", Email: "john@example.com"}
response := Response{
Success: true,
Message: "User retrieved successfully",
Data: user,
}

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

HTML Responses

Render HTML templates with Echo's template engine.

type Template struct {
templates *template.Template
}

func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}

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

// Setup template renderer
t := &Template{
templates: template.Must(template.ParseGlob("templates/*.html")),
}
e.Renderer = t

e.GET("/", homePage)
e.GET("/users", usersPage)

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

func homePage(c echo.Context) error {
data := map[string]interface{}{
"title": "Home Page",
"users": []User{
{ID: 1, Name: "John", Email: "john@example.com"},
{ID: 2, Name: "Jane", Email: "jane@example.com"},
},
}

return c.Render(http.StatusOK, "home.html", data)
}

func usersPage(c echo.Context) error {
data := map[string]interface{}{
"title": "Users Page",
"users": []User{
{ID: 1, Name: "John", Email: "john@example.com"},
{ID: 2, Name: "Jane", Email: "jane@example.com"},
},
}

return c.Render(http.StatusOK, "users.html", data)
}

File Serving

Serve static files and handle downloads.

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

// Serve static files
e.Static("/static", "assets")
e.File("/favicon.ico", "assets/favicon.ico")

// Serve files with custom headers
e.GET("/download/:filename", downloadFile)

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

func downloadFile(c echo.Context) error {
filename := c.Param("filename")
filepath := "downloads/" + filename

// Check if file exists
if _, err := os.Stat(filepath); os.IsNotExist(err) {
return c.JSON(http.StatusNotFound, map[string]string{
"error": "File not found",
})
}

// Set headers for download
c.Response().Header().Set("Content-Disposition", "attachment; filename="+filename)
c.Response().Header().Set("Content-Type", "application/octet-stream")

return c.File(filepath)
}

Route Grouping

Basic Grouping

Organize routes into logical groups for better maintainability.

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

// API v1 group
v1 := e.Group("/api/v1")
v1.GET("/users", getUsers)
v1.POST("/users", createUser)
v1.GET("/users/:id", getUser)
v1.PUT("/users/:id", updateUser)
v1.DELETE("/users/:id", deleteUser)

// Admin group
admin := e.Group("/admin")
admin.GET("/dashboard", adminDashboard)
admin.GET("/users", adminGetUsers)
admin.POST("/users", adminCreateUser)

// Public group
public := e.Group("/public")
public.GET("/info", publicInfo)
public.GET("/health", healthCheck)

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

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

func adminGetUsers(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "Admin Get Users"})
}

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

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

func healthCheck(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"status": "healthy"})
}

Nested Groups

Create nested route groups for complex applications.

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

// API group
api := e.Group("/api")

// API v1 group
v1 := api.Group("/v1")

// Users group under v1
users := v1.Group("/users")
users.GET("", getUsers)
users.POST("", createUser)
users.GET("/:id", getUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)

// Posts group under v1
posts := v1.Group("/posts")
posts.GET("", getPosts)
posts.POST("", createPost)
posts.GET("/:id", getPost)
posts.PUT("/:id", updatePost)
posts.DELETE("/:id", deletePost)

// API v2 group
v2 := api.Group("/v2")
v2.GET("/users", getUsersV2)
v2.POST("/users", createUserV2)

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

Middleware

Built-in Middleware

Use Echo's built-in middleware for common functionality.

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

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

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

// Recover middleware (handles panics)
e.Use(middleware.Recover())

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

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

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

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

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

e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, Echo!")
})

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

Custom Middleware

Create custom middleware for application-specific logic.

// Authentication middleware
func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")
if token == "" {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Authorization header required",
})
}

// Validate token (simplified)
if token != "Bearer valid-token" {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid token",
})
}

// Set user info in context
c.Set("user_id", "123")
c.Set("user_role", "admin")

return next(c)
}
}

// Logging middleware
func LoggingMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
start := time.Now()

err := next(c)

req := c.Request()
res := c.Response()

log.Printf(
"Method: %s, URI: %s, Status: %d, Latency: %v",
req.Method,
req.RequestURI,
res.Status,
time.Since(start),
)

return err
}
}

// Role-based access control middleware
func RequireRole(role string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
userRole := c.Get("user_role").(string)
if userRole != role {
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Insufficient permissions",
})
}
return next(c)
}
}
}

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

// Apply global middleware
e.Use(LoggingMiddleware)

// Public routes
e.GET("/", homePage)
e.POST("/login", login)

// Protected routes
protected := e.Group("/api")
protected.Use(AuthMiddleware)

// Admin routes
admin := protected.Group("/admin")
admin.Use(RequireRole("admin"))
admin.GET("/dashboard", adminDashboard)
admin.GET("/users", adminGetUsers)

// User routes
user := protected.Group("/user")
user.Use(RequireRole("user"))
user.GET("/profile", getUserProfile)
user.PUT("/profile", updateUserProfile)

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

Validation

Built-in Validation

Use Echo's built-in validation with struct tags.

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

type User struct {
ID int `json:"id" validate:"omitempty"`
Name string `json:"name" validate:"required,min=2,max=50"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=130"`
Password string `json:"password" validate:"required,min=8,containsany=!@#$%^&*"`
Website string `json:"website" validate:"omitempty,url"`
Phone string `json:"phone" validate:"omitempty,e164"`
}

type Product struct {
ID int `json:"id" validate:"omitempty"`
Name string `json:"name" validate:"required,min=1,max=100"`
Price float64 `json:"price" validate:"required,gt=0"`
Category string `json:"category" validate:"required,oneof=electronics clothing books"`
InStock bool `json:"in_stock"`
StockCount int `json:"stock_count" validate:"gte=0"`
Tags []string `json:"tags" validate:"omitempty,dive,min=1,max=20"`
}

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

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

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

// Process user creation...
user.ID = 1

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

Custom Validators

Create custom validation functions for complex business rules.

import (
"github.com/go-playground/validator/v10"
"regexp"
)

// Custom validator for strong password
func validateStrongPassword(fl validator.FieldLevel) bool {
password := fl.Field().String()

// At least 8 characters, 1 uppercase, 1 lowercase, 1 number, 1 special character
var (
hasUpper = regexp.MustCompile(`[A-Z]`).MatchString(password)
hasLower = regexp.MustCompile(`[a-z]`).MatchString(password)
hasNumber = regexp.MustCompile(`[0-9]`).MatchString(password)
hasSpecial = regexp.MustCompile(`[!@#$%^&*]`).MatchString(password)
)

return len(password) >= 8 && hasUpper && hasLower && hasNumber && hasSpecial
}

// Custom validator for unique email
func validateUniqueEmail(fl validator.FieldLevel) bool {
email := fl.Field().String()

// Simulate database check
existingEmails := []string{"john@example.com", "jane@example.com"}
for _, existing := range existingEmails {
if existing == email {
return false
}
}

return true
}

// Custom validator for future date
func validateFutureDate(fl validator.FieldLevel) bool {
date := fl.Field().Interface().(time.Time)
return date.After(time.Now())
}

func setupCustomValidators(e *echo.Echo) {
v := validator.New()

// Register custom validators
v.RegisterValidation("strong_password", validateStrongPassword)
v.RegisterValidation("unique_email", validateUniqueEmail)
v.RegisterValidation("future_date", validateFutureDate)

e.Validator = &CustomValidator{validator: v}
}

type CustomValidator struct {
validator *validator.Validate
}

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

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

// Setup custom validators
setupCustomValidators(e)

e.POST("/users", createUserWithCustomValidation)

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

type UserWithCustomValidation struct {
Name string `json:"name" validate:"required,min=2,max=50"`
Email string `json:"email" validate:"required,email,unique_email"`
Password string `json:"password" validate:"required,strong_password"`
BirthDate time.Time `json:"birth_date" validate:"required,future_date"`
}

func createUserWithCustomValidation(c echo.Context) error {
user := new(UserWithCustomValidation)

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

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

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

Request Validation

Validate query parameters and path parameters.

type UserQuery struct {
Page int `query:"page" validate:"gte=1"`
Limit int `query:"limit" validate:"gte=1,lte=100"`
Search string `query:"search" validate:"omitempty,min=1,max=50"`
SortBy string `query:"sort_by" validate:"omitempty,oneof=name email created_at"`
SortDir string `query:"sort_dir" validate:"omitempty,oneof=asc desc"`
}

func getUsers(c echo.Context) error {
query := new(UserQuery)

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

if err := c.Validate(query); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{
"error": "Validation failed",
"details": err.Error(),
})
}

// Set defaults
if query.Page == 0 {
query.Page = 1
}
if query.Limit == 0 {
query.Limit = 10
}
if query.SortBy == "" {
query.SortBy = "created_at"
}
if query.SortDir == "" {
query.SortDir = "desc"
}

// Simulate database query
users := []User{
{ID: 1, Name: "John", Email: "john@example.com"},
{ID: 2, Name: "Jane", Email: "jane@example.com"},
}

return c.JSON(http.StatusOK, map[string]interface{}{
"users": users,
"pagination": map[string]interface{}{
"page": query.Page,
"limit": query.Limit,
"total": len(users),
},
"filters": map[string]interface{}{
"search": query.Search,
"sort_by": query.SortBy,
"sort_dir": query.SortDir,
},
})
}

Advanced Topics

Database Integration

Integrate Echo with databases using GORM.

import (
"gorm.io/gorm"
"gorm.io/driver/postgres"
)

type User struct {
gorm.Model
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email" gorm:"unique"`
Password string `json:"password" validate:"required,min=6"`
}

type UserHandler struct {
db *gorm.DB
}

func NewUserHandler(db *gorm.DB) *UserHandler {
return &UserHandler{db: db}
}

func (h *UserHandler) GetUsers(c echo.Context) error {
var users []User
result := h.db.Find(&users)
if result.Error != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to fetch users",
})
}

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

func (h *UserHandler) GetUser(c echo.Context) error {
id := c.Param("id")
var user User

result := h.db.First(&user, id)
if result.Error != nil {
return c.JSON(http.StatusNotFound, map[string]string{
"error": "User not found",
})
}

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

func (h *UserHandler) CreateUser(c echo.Context) error {
user := new(User)

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

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

result := h.db.Create(user)
if result.Error != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to create user",
})
}

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

func main() {
// Connect to database
dsn := "host=localhost user=postgres password=password dbname=myapp port=5432 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}

// Auto migrate
db.AutoMigrate(&User{})

e := echo.New()

// Create handler
userHandler := NewUserHandler(db)

// Routes
e.GET("/users", userHandler.GetUsers)
e.GET("/users/:id", userHandler.GetUser)
e.POST("/users", userHandler.CreateUser)

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

Error Handling

Implement comprehensive error handling for your application.

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

// Custom error types
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidInput = errors.New("invalid input")
ErrUnauthorized = errors.New("unauthorized")
ErrForbidden = errors.New("forbidden")
ErrInternalServer = errors.New("internal server error")
)

// Custom error handler
func customErrorHandler(err error, c echo.Context) {
var statusCode int
var message string

switch {
case errors.Is(err, ErrUserNotFound):
statusCode = http.StatusNotFound
message = "User not found"
case errors.Is(err, ErrInvalidInput):
statusCode = http.StatusBadRequest
message = "Invalid input"
case errors.Is(err, ErrUnauthorized):
statusCode = http.StatusUnauthorized
message = "Unauthorized"
case errors.Is(err, ErrForbidden):
statusCode = http.StatusForbidden
message = "Forbidden"
case errors.Is(err, ErrInternalServer):
statusCode = http.StatusInternalServerError
message = "Internal server error"
default:
statusCode = http.StatusInternalServerError
message = "Something went wrong"
}

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

// Return JSON response
c.JSON(statusCode, map[string]interface{}{
"error": message,
"code": statusCode,
"success": false,
})
}

func getUser(c echo.Context) error {
id := c.Param("id")

// Simulate user not found
if id == "999" {
return ErrUserNotFound
}

user := User{ID: 1, Name: "John", Email: "john@example.com"}
return c.JSON(http.StatusOK, user)
}

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

// Set custom error handler
e.HTTPErrorHandler = customErrorHandler

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

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

Configuration Management

Manage application configuration with environment variables and config files.

import (
"github.com/spf13/viper"
)

type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
JWT JWTConfig `mapstructure:"jwt"`
}

type ServerConfig struct {
Port string `mapstructure:"port"`
Host string `mapstructure:"host"`
}

type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port string `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Name string `mapstructure:"name"`
SSLMode string `mapstructure:"ssl_mode"`
}

type JWTConfig struct {
Secret string `mapstructure:"secret"`
Expiry int `mapstructure:"expiry"`
}

func loadConfig() (*Config, error) {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath("./config")

// Environment variables
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

// Default values
viper.SetDefault("server.port", "8080")
viper.SetDefault("server.host", "localhost")
viper.SetDefault("database.ssl_mode", "disable")
viper.SetDefault("jwt.expiry", 24)

if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, err
}
}

var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, err
}

return &config, nil
}

func main() {
config, err := loadConfig()
if err != nil {
log.Fatal(err)
}

e := echo.New()

// Use configuration
addr := config.Server.Host + ":" + config.Server.Port
e.Logger.Fatal(e.Start(addr))
}

Testing

Write comprehensive tests for your Echo application.

import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)

func TestGetUsers(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/users", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

if assert.NoError(t, getUsers(c)) {
assert.Equal(t, http.StatusOK, rec.Code)

var response map[string]interface{}
err := json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "Users retrieved successfully", response["message"])
}
}

func TestCreateUser(t *testing.T) {
e := echo.New()

userJSON := `{
"name": "John Doe",
"email": "john@example.com",
"password": "password123"
}`

req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(userJSON))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

if assert.NoError(t, createUser(c)) {
assert.Equal(t, http.StatusCreated, rec.Code)

var user User
err := json.Unmarshal(rec.Body.Bytes(), &user)
assert.NoError(t, err)
assert.Equal(t, "John Doe", user.Name)
assert.Equal(t, "john@example.com", user.Email)
}
}

func TestCreateUserValidation(t *testing.T) {
e := echo.New()

// Invalid user data (missing required fields)
userJSON := `{
"name": "",
"email": "invalid-email",
"password": "123"
}`

req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(userJSON))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

if assert.NoError(t, createUser(c)) {
assert.Equal(t, http.StatusBadRequest, rec.Code)

var response map[string]interface{}
err := json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "Validation failed", response["error"])
}
}

// Integration test
func TestUserAPI(t *testing.T) {
e := echo.New()

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

// Test server
server := httptest.NewServer(e)
defer server.Close()

// Test GET /users
resp, err := http.Get(server.URL + "/users")
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)

// Test POST /users
userJSON := `{"name":"Jane","email":"jane@example.com","password":"password123"}`
resp, err = http.Post(server.URL+"/users", echo.MIMEApplicationJSON, strings.NewReader(userJSON))
assert.NoError(t, err)
assert.Equal(t, http.StatusCreated, resp.StatusCode)

// Test GET /users/:id
resp, err = http.Get(server.URL + "/users/1")
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
}

Best Practices

Project Structure

Organize your Echo application with a clean, scalable structure.

myapp/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── handlers/
│ │ ├── user.go
│ │ └── product.go
│ ├── middleware/
│ │ ├── auth.go
│ │ ├── logging.go
│ │ └── cors.go
│ ├── models/
│ │ ├── user.go
│ │ └── product.go
│ ├── repository/
│ │ ├── user.go
│ │ └── product.go
│ ├── service/
│ │ ├── user.go
│ │ └── product.go
│ └── routes/
│ └── routes.go
├── pkg/
│ ├── database/
│ │ └── database.go
│ ├── config/
│ │ └── config.go
│ └── utils/
│ └── utils.go
├── configs/
│ ├── config.yaml
│ └── config.prod.yaml
├── docs/
│ └── swagger.json
├── scripts/
│ ├── build.sh
│ └── deploy.sh
├── tests/
│ ├── handlers_test.go
│ └── integration_test.go
├── go.mod
├── go.sum
└── README.md

Performance Optimization

Optimize your Echo application for better performance.

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

// Performance optimizations
e.HideBanner = true
e.HidePort = true

// Configure server
e.Server.ReadTimeout = 30 * time.Second
e.Server.WriteTimeout = 30 * time.Second
e.Server.IdleTimeout = 120 * time.Second

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

// Caching headers
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set("Cache-Control", "public, max-age=3600")
return next(c)
}
})

// Connection pooling for database
// Use connection pools in your database configuration

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

Security Best Practices

Implement security best practices in your Echo application.

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

// Security middleware
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
XSSProtection: "1; mode=block",
ContentTypeNosniff: "nosniff",
XFrameOptions: "SAMEORIGIN",
HSTSMaxAge: 3600,
ContentSecurityPolicy: "default-src 'self'",
}))

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

// Request size limiting
e.Use(middleware.BodyLimit("1M"))

// CORS with strict configuration
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://yourdomain.com"},
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
MaxAge: 86400,
}))

// Input sanitization middleware
e.Use(sanitizeInput)

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

func sanitizeInput(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Sanitize query parameters
for key, values := range c.QueryParams() {
for i, value := range values {
values[i] = html.EscapeString(value)
}
c.QueryParams()[key] = values
}

return next(c)
}
}

Summary

Echo is a powerful, high-performance web framework for Go that provides:

Core Features:

  • High Performance: Optimized for speed and low memory usage
  • Extensible: Rich middleware ecosystem and plugin system
  • Validation: Built-in request validation with custom validators
  • Grouping: Route grouping for better organization
  • Binding: Automatic request/response binding
  • Rendering: Template rendering and static file serving

Best Practices:

  • Use middleware for cross-cutting concerns
  • Implement proper validation for all inputs
  • Structure your application with clear separation of concerns
  • Write comprehensive tests
  • Follow security best practices
  • Optimize for performance

Advanced Topics:

  • Database integration with GORM
  • Custom error handling
  • Configuration management
  • Comprehensive testing strategies
  • Security implementations
  • Performance optimization

Echo provides an excellent foundation for building scalable, maintainable web applications in Go with its clean API and powerful features.