Building an API with Echo
Why Echo?
Section titled “Why Echo?”If you come from Node.js you already think in terms of a lightweight HTTP framework: a function that maps a path + verb to a handler, a middleware pipeline, and a way to serialize JSON. Echo is that framework for Go.
| Concern | Node.js / Express | Go / Echo |
|---|---|---|
| Core abstraction | (req, res, next) functions | func(c *echo.Context) error |
| Routing | app.get('/path', handler) | e.GET("/path", handler) |
| Middleware | app.use(fn) | e.Use(fn) |
| JSON response | res.json({ … }) | c.JSON(200, payload) |
| Error propagation | next(err) | return err |
Echo competes with frameworks like Fastify and NestJS in spirit, but it is a single compiled binary with no runtime, no VM, and no node_modules. Cold-start time is microseconds, not seconds.
Echo vs the Go ecosystem
Section titled “Echo vs the Go ecosystem”Go has several web frameworks (Gin, Chi, Fiber, stdlib net/http). Echo sits in the sweet spot:
- Minimal surface area — you can read the entire source in a weekend.
- Idiomatic — handlers return
error; Go’s error-as-value pattern maps directly. - Rich built-ins — Logger, Recover, CORS, GZIP, BasicAuth out of the box.
- Stable — Echo v5 (2026) is the current major; v4 keeps receiving security fixes until the end of 2026.
Coming from Echo v4?
Section titled “Coming from Echo v4?”Most tutorials on the web still show v4. This module uses v5 (github.com/labstack/echo/v5, Go 1.25+). The differences you will notice:
| v4 | v5 |
|---|---|
func(c echo.Context) error — Context is an interface | func(c *echo.Context) error — Context is a struct |
middleware.Logger(), custom e.Logger interface | middleware.RequestLogger(), e.Logger is *slog.Logger |
e.Logger.Fatal(e.Start(":1323")) | if err := e.Start(":1323"); err != nil { … } (or echo.StartConfig for graceful shutdown) |
e.HTTPErrorHandler = func(err error, c echo.Context) | e.HTTPErrorHandler = func(c *echo.Context, err error) |
err.(*echo.HTTPError) on echo.ErrNotFound | echo.StatusCode(err) — predefined errors are no longer *HTTPError |
c.ParamNames() / c.ParamValues() | c.PathValues(); typed echo.PathParam[int](c, "id") |
Two sed one-liners do most of a migration: replace echo/v4 with echo/v5, and echo.Context with *echo.Context.
// Express (Node.js)import express from 'express';const app = express();app.use(express.json());
app.get('/ping', (req, res) => { res.json({ message: 'pong' });});
app.listen(1323, () => console.log('listening'));// Echo (Go)package main
import ( "log/slog" "net/http" "os"
"github.com/labstack/echo/v5")
func main() { e := echo.New()
e.GET("/ping", func(c *echo.Context) error { return c.JSON(http.StatusOK, map[string]string{ "message": "pong", }) })
if err := e.Start(":1323"); err != nil { slog.Error("server stopped", "err", err) os.Exit(1) }}What you will build
Section titled “What you will build”Throughout this module you will build a small Books REST API — create, read, update, and delete books — step by step:
- Scaffold a project with
go.modandmain.go. - Define routes and handler functions.
- Add middleware (logging, recovery, CORS).
- Bind request bodies and validate them with struct tags.
- Shape JSON responses with status codes.
- Handle errors centrally — the Go way.
- Touch a database via
database/sql. - Write handler tests with
httptest.
Each lesson is self-contained. You can jump in at any step, but the project grows progressively.