Featured image of post

High performance, minimalist Go web framework.

Echo: A High‑Performance, Minimalist Go Web Framework

Introduction

Echo (github.com/labstack/echo) is a lightweight, yet powerful web framework for Go that focuses on speed, simplicity, and extensibility. With 32,228 stars on GitHub, it has become one of the most popular choices for building HTTP services, APIs, and micro‑services in the Go ecosystem.

Developers reach for Echo when they need:

  • High throughput – Echo’s router is optimized for low allocation and fast lookup, making it suitable for services that handle tens of thousands of requests per second.
  • Minimal boilerplate – The core API consists of a few structs and methods, letting you get a server up and running in under ten lines of code.
  • Rich middleware ecosystem – Built‑in support for logging, recovery, CORS, JWT, rate limiting, request ID, and more means you can add cross‑cutting concerns without pulling in extra dependencies.
  • Easy extensibility – You can write custom middleware, bind custom renderers, or plug in your own validator with virtually zero friction.

Typical real‑world use cases include RESTful JSON APIs for mobile back‑ends, internal micro‑service communication, admin dashboards, and even simple static‑file servers with dynamic routing. Echo solves the common pain points of verbose net/http code, repetitive error handling, and the need to assemble a suite of third‑party packages just to get logging, CORS, and JWT working together.


Key Features Echo packs a lot of capability into a small API surface. Below are the features that make it stand out, each explained with why it matters and how it compares to the standard library.

Feature What It Does Why It’s Valuable Standard Go Equivalent
High‑performance router Uses a radix tree (trie) for O(log n) route lookup, with zero‑allocation path parameters. Faster than the default ServeMux for complex routing tables; scales well with many static and parameterized routes. http.ServeMux (linear scan)
Middleware chaining Middleware are func(echo.HandlerFunc) echo.HandlerFunc; can be applied globally, per‑group, or per‑route. Centralizes cross‑cutting concerns (logging, auth, CORS) and keeps handlers clean. Manual wrapping of http.Handler
Built‑in logger & recovery echo.Logger (zerolog‑compatible) and middleware.Recover() recover panics and log stack traces. Eliminates boilerplate for request‑scoped logging and prevents crashes from taking down the whole server. log.Printf + custom recover
Automatic HTTPS & HTTP/2 e.StartTLS or e.AutoTLS (via golang.org/x/crypto/acme/autocert) handles cert provisioning. One‑line production‑grade TLS setup; HTTP/2 comes for free. Manual http.ListenAndServeTLS + cert management
Data binding & validation e.Bind(&payload) uses tags (json, form, xml) and integrates with go-playground/validator.v10. Reduces repetitive json.Decoder code and provides declarative validation with customizable error messages. Manual decoding + validation
Template rendering Supports HTML/template, Jet, Amber, etc., via e.Renderer. Enables server‑side rendering without leaving Echo; swap renderers by implementing a simple interface. html/template package directly
Graceful shutdown e.Shutdown(ctx) waits for active connections to finish before exiting. Essential for zero‑downtime deployments and container orchestrators (Kubernetes). http.Server.Shutdown (requires manual server creation)
WebSocket support (via middleware) middleware.WebSocketWithConfig upgrades connections and lets you define a handler func. Real‑time features (chat, notifications) become a few lines of code. Manual http.Hijacker + upgrade logic

These features combine to give you a “batteries‑included” experience while staying true to Go’s philosophy of small, composable packages.


Installation and Setup

Echo is distributed as a Go module. The current stable major version is v4, which requires Go 1.18+ (modules are mandatory).

1# Initialize a module if you don’t have one yet
2go mod init example.com/echo-demo
3
4# Add Echo v4 as a dependencygo get github.com/labstack/echo/v4

Verification

Create a file main.go with the minimal Hello World (see the next section) and run:

You should see output similar to:

1⇨ http server started on [::]:1323

Visiting http://localhost:1323 in a browser or with curl returns Hello, World!. If the server starts without errors, Echo is correctly installed.


Basic Usage

Below is the simplest possible Echo program. Every line is annotated to show what it does.

 1package main
 2
 3import (
 4	"github.com/labstack/echo/v4"
 5	"github.com/labstack/echo/v4/middleware"
 6)
 7
 8func main() {
 9	// 1️⃣ Create a new Echo instance.
10	e := echo.New()
11
12	// 2️⃣ Attach global middleware: logger and recovery.
13	e.Use(middleware.Logger())
14	e.Use(middleware.Recover())
15
16	// 3️⃣ Define a route. Echo’s router supports HTTP verbs as methods.
17	e.GET("/", func(c echo.Context) error {
18		// Context gives you access to request, response, and helpers.
19		return c.String(200, "Hello, World!")
20	})
21
22	// 4️⃣ Start the server on port 1323. ListenAddr can be ":8080", "127.0.0.1:8080", etc.
23	//    The method blocks until the program is interrupted or Shutdown is called.
24	if err := e.Start(":1323"); err != nil {
25		e.Logger.Fatalf("shutting down the server: %v", err)
26	}
27}

Explanation

  1. echo.New() creates the core router and holds middleware, groups, and routes.
  2. middleware.Logger() writes a formatted access log to os.Stdout; middleware.Recover() catches panics, logs the stack, and returns a 500 response.
  3. e.GET registers a handler for the root path. The handler receives an echo.Context, which abstracts *http.Request and http.ResponseWriter.
  4. e.Start binds the listener and begins accepting connections. On error (e.g., port already in use) we log and exit.

Expected output (terminal):

1⇨ http server started on [::]:1323

curl test

1HTTP/1.1 200 OK
2Content-Length: 13
3Content-Type: text/plain; charset=UTF-8
4Date: ...
5
6Hello, World!

Real‑World Examples

The following three examples illustrate production‑grade usage of Echo. Each program is self‑contained, includes proper error handling, logging, and demonstrates features you would typically need in a service.

Note: All examples assume you have initialized a Go module (go mod init) and have run go get for the dependencies shown in the imports.

Example 1 – JWT‑Protected Todo REST API (CRUD + Validation)

This example builds a tiny todo service:

  • Endpoints

    • POST /todos – create a todo (requires JWT)
    • GET /todos – list all todos (requires JWT)
    • GET /todos/:id – get a single todo
    • PUT /todos/:id – update a todo
    • DELETE /todos/:id – delete a todo
  • Middleware

    • middleware.Logger() and middleware.Recover() (global) * middleware.JWTWithConfig for protecting /todos* routes
    • Custom validator using go-playground/validator/v10 for request payloads
  • Data store – an in‑memory map protected by a sync.RWMutex (good for demo; replace with a real DB in production).

  1package main
  2
  3import (
  4	"errors"
  5	"net/http"
  6	"sync"
  7	"time"
  8
  9	"github.com/dgrijalva/jwt-go"
 10	"github.com/labstack/echo/v4"
 11	"github.com/labstack/echo/v4/middleware"
 12	"gopkg.in/go-playground/validator.v9"
 13)
 14
 15// ---------- Model ----------
 16type Todo struct {
 17	ID   int    `json:"id" validate:"required,gte=1"`
 18	Title string `json:"title" validate:"required,min=3,max=100"`
 19	Done bool   `json:"done"`
 20}
 21
 22// ---------- In‑memory store ----------
 23type todoStore struct {
 24	mu   sync.RWMutex
 25	data map[int]*Todo
 26	next int
 27}
 28
 29func newTodoStore() *todoStore {
 30	return &todoStore{data: make(map[int]*Todo), next: 1}
 31}
 32
 33func (s *todoStore) Create(t *Todo) int {
 34	s.mu.Lock()
 35	defer s.mu.Unlock()
 36	t.ID = s.next
 37	s.next++
 38	s.data[t.ID] = t
 39	return t.ID
 40}
 41
 42func (s *todoStore) Get(id int) (*Todo, bool) {
 43	s.mu.RLock()
 44	defer s.mu.RUnlock()
 45	t, ok := s.data[id]
 46	return t, ok
 47}
 48
 49func (s *todoStore) Update(id int, t *Todo) error {
 50	s.mu.Lock()
 51	defer s.mu.Unlock()
 52	if _, ok := s.data[id]; !ok {
 53		return errors.New("not found")
 54	}
 55	t.ID = id	s.data[id] = t
 56	return nil}
 57
 58func (s *todoStore) Delete(id int) error {
 59	s.mu.Lock()
 60	defer s.mu.Unlock()
 61	if _, ok := s.data[id]; !ok {
 62		return errors.New("not found")
 63	}
 64	delete(s.data, id)
 65	return nil
 66}
 67
 68func (s *todoStore) List() []*Todo {
 69	s.mu.RLock()
 70	defer s.mu.RUnlock()
 71	list := make([]*Todo, 0, len(s.data))
 72	for _, t := range s.data {
 73		list = append(list, t)
 74	}
 75	return list
 76}
 77
 78// ---------- Custom validator ----------
 79type customValidator struct {
 80	validator *validator.Validate
 81}
 82
 83func (cv *customValidator) Validate(i interface{}) error {
 84	if err := cv.validator.Struct(i); err != nil {
 85		// Return Echo's ValidationError type for proper JSON error response.
 86		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
 87	}
 88	return nil
 89}
 90
 91// ---------- JWT helper ----------
 92var jwtSecret = []byte("super-secret-key") // In production load from env or secret manager.
 93
 94func generateJWT(id int) (string, error) {
 95	claims := jwt.MapClaims{
 96		"user_id": id,
 97		"exp":     time.Now().Add(time.Hour * 72).Unix(),
 98	}
 99	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
100	return token.SignedString(jwtSecret)
101}
102
103// ---------- Main ----------
104func main() {
105	e := echo.New()
106	e.Use(middleware.Logger())
107	e.Use(middleware.Recover())
108
109	// Register custom validator.
110	e.Validator = &customValidator{validator: validator.New()}
111
112	// Public endpoint – login (returns JWT).
113	e.POST("/login", func(c echo.Context) error {
114		// In a real app you’d validate username/password against a DB.
115		// Here we accept any payload with a "username" field and fake user ID 1.
116		type loginReq struct {
117			Username string `json:"username" validate:"required,alphanum"`
118		}
119		var req loginReq
120		if err := c.Bind(&req); err != nil {
121			return err
122		}
123		if err := c.Validate(req); err != nil {
124			return err
125		}
126		token, err := generateJWT(1) // user ID 1 for demo
127		if err != nil {
128			return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate token")
129		}
130		return c.JSON(http.StatusOK, map[string]string{"token": token})
131	})
132
133	// Protected todo routes.
134	r := e.Group("/todos")
135	config := middleware.JWTConfig{
136		Claims:     &jwt.MapClaims{},
137		SigningKey: jwtSecret,
138	}
139	r.Use(middleware.JWTWithConfig(config))
140
141	store := newTodoStore()
142
143	r.POST("", func(c echo.Context) error {
144		var t Todo
145		if err := c.Bind(&t); err != nil {
146			return err
147		}
148		if err := c.Validate(t); err != nil {
149			return err
150		}
151		id := store.Create(&t)
152		return c.JSON(http.StatusCreated, map[string]int{"id": id})
153	})
154
155	r.GET("", func(c echo.Context) error {
156		todos := store.List()
157		return c.JSON(http.StatusOK, todos)
158	})
159
160	r.GET("/:id", func(c echo.Context) error {
161		id := c.Param("id")
162		var tid int
163		if _, err := fmt.Sscanf(id, "%d", &tid); err != nil {
164			return echo.NewHTTPError(http.StatusBadRequest, "invalid id")
165		}
166		t, ok := store.Get(tid)
167		if !ok {
168			return echo.NewHTTPError(http.StatusNotFound, "todo not found")
169		}
170		return c.JSON(http.StatusOK, t)
171	})
172
173	r.PUT("/:id", func(c echo.Context) error {
174		id := c.Param("id")
175		var tid int
176		if _, err := fmt.Sscanf(id, "%d", &tid); err != nil {
177			return echo.NewHTTPError(http.StatusBadRequest, "invalid id")
178		}
179		var t Todo
180		if err := c.Bind(&t); err != nil {
181			return err
182		}
183		if err := c.Validate(t); err != nil {
184			return err
185		}
186		if err := store.Update(tid, &t); err != nil {
187			return echo.NewHTTPError(http.StatusNotFound, err.Error())
188		}
189		return c.NoContent(http.StatusOK)
190	})
191
192	r.DELETE("/:id", func(c echo.Context) error {
193		id := c.Param("id")
194		var tid int
195		if _, err := fmt.Sscanf(id, "%d", &tid); err != nil {
196			return echo.NewHTTPError(http.StatusBadRequest, "invalid id")
197		}
198		if err := store.Delete(tid); err != nil {
199			return echo.NewHTTPError(http.StatusNotFound, err.Error())
200		}
201		return c.NoContent(http.StatusNoContent)
202	})
203
204	// Graceful shutdown.
205	go func() {
206		if err := e.Start(":1323"); err != nil && err != http.ErrServerClosed {
207			e.Logger.Fatalf("shutting down the server: %v", err)
208		}
209	}()
210
211	// Wait for interrupt signal to gracefully shutdown.
212	quit := make(chan os.Signal, 1)
213	signal.Notify(quit, os.Interrupt)
214	<-quit
215	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
216	defer cancel()
217	if err := e.Shutdown(ctx); err != nil {
218		e.Logger.Fatal(err)
219	}
220}

How to run

1go run .

Sample interaction

 1# 1️⃣ Obtain a token (any username works)
 2$ curl -X POST http://localhost:1323/login -H "Content-Type: application/json" \
 3   -d '{"username":"alice"}'
 4{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
 5
 6# 2️⃣ Create a todo$ curl -X POST http://localhost:1323/todos \
 7   -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
 8   -H "Content-Type: application/json" \
 9   -d '{"title":"Buy milk"}'
10{"id":1}
11
12# 3️⃣ List todos
13$ curl -H "Authorization: Bearer <same token>" http://localhost:1323/todos
14[{"id":1,"title":"Buy milk","done":false}]

The example demonstrates:

  • Middleware stacking (logger → recover → JWT).
  • Custom validator returning Echo’s HTTPError for clean JSON validation errors.
  • Stateless JWT authentication (no sessions).
  • Graceful shutdown on SIGINT.

Example

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy