Web Backend Development on Go (Echo)
Echo and Gin solve same task, different approaches. Echo emphasizes extensibility: middleware, context, binder—all interfaces replaceable. Gin slightly faster in benchmarks, Echo slightly more convenient architecturally, especially writing middleware. Real projects—performance difference negligible, bottleneck always DB, not router.
Choose Echo for: convenient route grouping API, built-in Validator interface, good WebSocket/SSE support, readable middleware code.
Initialization and Routes
package server
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/myapp/internal/domain/product"
"github.com/myapp/internal/middleware"
)
type Server struct {
echo *echo.Echo
product *product.Handler
}
func New(deps Dependencies) *Server {
e := echo.New()
e.HideBanner = true
e.Validator = middleware.NewValidator()
// Built-in middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(100)))
s := &Server{echo: e, product: product.NewHandler(deps)}
s.registerRoutes()
return s
}
func (s *Server) registerRoutes() {
api := s.echo.Group("/api/v1")
// Public
authGroup := api.Group("/auth")
authGroup.POST("/login", s.product.Login)
// Protected
restricted := api.Group("", middleware.JWT())
restricted.GET("/profile", s.product.Profile)
// Products
products := api.Group("/products")
products.GET("", s.product.List)
products.GET("/:id", s.product.Get)
}
Handler (Controller)
package product
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/myapp/internal/domain"
)
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
func (h *Handler) List(c echo.Context) error {
page := c.QueryParamDefault("page", "1")
limit := c.QueryParamDefault("limit", "20")
products, total, err := h.service.List(c.Request().Context(), page, limit)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"data": products,
"total": total,
})
}
func (h *Handler) Get(c echo.Context) error {
id := c.Param("id")
product, err := h.service.GetByID(c.Request().Context(), id)
if err != nil {
if err == domain.ErrNotFound {
return c.JSON(http.StatusNotFound, map[string]string{"error": "Not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, product)
}
Service (Business Logic)
package product
import (
"context"
"github.com/myapp/internal/domain"
)
type Service struct {
repo domain.ProductRepository
}
func NewService(repo domain.ProductRepository) *Service {
return &Service{repo: repo}
}
func (s *Service) List(ctx context.Context, page, limit string) ([]domain.Product, int, error) {
products, err := s.repo.FindMany(ctx)
if err != nil {
return nil, 0, err
}
return products, len(products), nil
}
func (s *Service) GetByID(ctx context.Context, id string) (*domain.Product, error) {
return s.repo.FindByID(ctx, id)
}
Database Layer
package repository
import (
"context"
"database/sql"
"github.com/myapp/internal/domain"
)
type ProductRepository struct {
db *sql.DB
}
func NewProductRepository(db *sql.DB) *ProductRepository {
return &ProductRepository{db: db}
}
func (r *ProductRepository) FindByID(ctx context.Context, id string) (*domain.Product, error) {
var p domain.Product
err := r.db.QueryRowContext(
ctx,
"SELECT id, name, price, created_at FROM products WHERE id = $1",
id,
).Scan(&p.ID, &p.Name, &p.Price, &p.CreatedAt)
if err == sql.ErrNoRows {
return nil, domain.ErrNotFound
}
return &p, err
}
Custom Middleware
package middleware
import (
"github.com/labstack/echo/v4"
"github.com/golang-jwt/jwt/v5"
)
func JWT() 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
_, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
return []byte("secret"), nil
})
if err != nil {
return c.JSON(401, map[string]string{"error": "Invalid token"})
}
return next(c)
}
}
}
Timeline
Basic setup: routes, handlers, middleware—1 day. Database integration, business logic—2–3 days. Complete API with tests—1 week.
Go/Echo advantages: compiled binary, single deploy, excellent performance, static typing catches errors early.







