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.