Skip to content

Building an API with 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.

ConcernNode.js / ExpressGo / Echo
Core abstraction(req, res, next) functionsfunc(c *echo.Context) error
Routingapp.get('/path', handler)e.GET("/path", handler)
Middlewareapp.use(fn)e.Use(fn)
JSON responseres.json({ … })c.JSON(200, payload)
Error propagationnext(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.

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.

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:

v4v5
func(c echo.Context) error — Context is an interfacefunc(c *echo.Context) error — Context is a struct
middleware.Logger(), custom e.Logger interfacemiddleware.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.ErrNotFoundecho.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.

TypeScript
// 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'));
Go
// 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)
}
}

Throughout this module you will build a small Books REST API — create, read, update, and delete books — step by step:

  1. Scaffold a project with go.mod and main.go.
  2. Define routes and handler functions.
  3. Add middleware (logging, recovery, CORS).
  4. Bind request bodies and validate them with struct tags.
  5. Shape JSON responses with status codes.
  6. Handle errors centrally — the Go way.
  7. Touch a database via database/sql.
  8. Write handler tests with httptest.

Each lesson is self-contained. You can jump in at any step, but the project grows progressively.

Which function creates a new Echo instance?
What is the return type of every Echo handler function?
Which method starts the Echo server on a given address?