What Echo Is & Your First Server
You already know Go, and you've maybe met Gin — the
most popular Go web framework. Echo is its closest peer: another fast, focused layer over the standard
library's net/http. If you've read the net/http roots guide,
you know Go can serve HTTP with nothing but the stdlib. Echo, like Gin, does the repetitive parts —
routing, JSON, middleware — without hiding what's underneath.
So why pick Echo over Gin, when they overlap so much? One design choice, mostly: in Echo, a handler
returns an error. Not func(c *gin.Context) with no return value, where you write the error response
by hand — but func(c echo.Context) error, where you hand the error back and a central handler turns it
into an HTTP response. That sounds small. In a real codebase with dozens of endpoints, it's the
difference between every handler re-implementing "how do I report a failure" and every handler
saying return err. Less boilerplate, and errors that come out consistent because one place renders
them all.
💡 Echo is net/http with helpers and opinions about errors. The helpers make it fast to write; the error-return style makes it stay clean as it grows. That's the whole pitch.
The mental model: instance holds routes, context handles the request, handlers return errors
Before any code, hold three things in your head. They are the entire framework.
📝 The instance (*echo.Echo, made with echo.New()) is your application. You create it once,
register all your routes and middleware on it, and start it. When someone says "the Echo app," they mean
this.
📝 The context (echo.Context) is one value handed to you for each incoming request. It carries
the request, the response writer, the path and query params, and every helper you use to read input and
write output. Note that echo.Context is an interface, not a struct — a detail that won't matter today
but is good to know.
📝 The handler returns an error. Every handler you write has the shape func(c echo.Context) error.
You do your work, then return c.JSON(...) on success or return someError on failure. Echo's central
error handler decides what the client actually sees.
Say it once: the instance holds the routes, the context handles the request, and the handler returns an error. Everything else in Echo is detail.
flowchart LR
E[echo.Echo<br/>holds the routes] --> R[route<br/>GET /ping]
R --> H["handler<br/>func(c echo.Context) error"]
H --> C[echo.Context<br/>reads input, writes output]
H -.returns error.-> X[central error handler<br/>renders the response]
One idea: the instance matches an incoming request to a route, calls that route's handler, and the handler uses the context to send a response — or returns an error for the central handler to render. Every Echo endpoint flows along those arrows.
Your first server
First, install Echo into your module. From inside your Go project:
What just happened: go get downloaded Echo (the /v4 is the current major version) and added it to
your go.mod/go.sum. The import path is github.com/labstack/echo/v4, and you refer to it in code as
the echo package. That one command is the whole install.
Now the smallest server that does something real. Create a file called main.go:
package main
What just happened: line by line —
echo.New()creates the instance and returns a*echo.Echo. We name ite.e.GET("/ping", ...)registers a route: when aGETrequest arrives for/ping, run the function we pass. That function is the handler, and its signature —func(c echo.Context) error— is the shape every Echo handler has.- Inside the handler,
c.JSON(http.StatusOK, ...)uses the context to write the response: it sets the status to200, setsContent-Typetoapplication/json, serializes the value, and sends it. Crucially,c.JSONreturns an error, and wereturnit — so if writing the response fails, Echo knows. On the happy path it returnsnil, which Echo reads as "all good." http.StatusOKis just the standard library's name for200. Echo leans onnet/http's constants rather than inventing its own — a reminder of what's underneath.e.Start(":1323")starts the server listening on port 1323 (Echo's docs use that port; nothing magic about it). It blocks, handling requests until you stop the program. We wrap it ine.Logger.Fatal(...)so that ifStartreturns an error — say the port is already taken — it's logged and the program exits instead of failing silently.
Run it like any Go program:
$ go run main.go
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4
⇨ http server started on [::]:1323
What just happened: go run compiled and started your program, and e.Start brought up the server.
Echo printed its banner and is now waiting for requests. Leave it running and, in another terminal, hit
the route:
$ curl localhost:1323/ping
{"message":"pong"}
What just happened: curl sent a GET /ping. The instance matched it to your route, called your
handler, and the handler used the context to write back JSON — returning nil to signal success. You
have a working JSON API in a dozen lines of real code.
The error-returning handler, and why it's different
This is the one place Echo and Gin diverge in a way worth pausing on. Put the two handler shapes side by side:
// Gin: no return value — you write the response (and any error) yourself.
// Echo: return an error — success or failure flows back to a central handler.
func error
What just happened: both write the same JSON. But the Echo version returns — and that return value is the hook. When something goes wrong, you don't have to write a status code and an error body by hand every time. You return an error, and Echo's central error handler renders it. Echo even gives you a purpose-built error type for this:
e
What just happened: instead of manually setting a 401 status and writing a JSON body, you returned
an *echo.HTTPError describing the failure. Echo's default error handler turns it into a clean 401
response with a JSON message. Every endpoint that returns this kind of error gets the same consistent
shape — no copy-pasted error-writing code.
⚠️ Don't worry about configuring that central handler yet — that's Phase 6's job, where we wire up a
custom HTTPErrorHandler for the books API. For now, just internalize the habit: in Echo, you
return your result, success or failure. Forgetting the return is the rookie Echo bug — the handler
compiles, but nothing gets sent and you stare at a hung request wondering why.
Adding Logger and Recover middleware
Here's a sharp difference from Gin worth knowing on day one. Gin's gin.Default() hands you a Logger and
a Recovery handler already wired up. Echo's echo.New() does not — it gives you a bare instance.
Logging and panic-recovery are opt-in. Most apps want both, so you add them yourself:
package main
What just happened: e.Use(...) registers middleware — code that runs around every request. We
imported github.com/labstack/echo/v4/middleware (a separate package from echo) and added two pieces:
middleware.Logger()prints a line for every request — method, path, status, how long it took. It's how you see your server working.middleware.Recover()catches a panic inside any handler, turns it into a clean500response, and keeps the server running. Without it, one panicking handler takes down the whole process.
We'll cover the middleware signature and write our own in Phase 5. ⚠️ The takeaway for now: unlike Gin, Echo doesn't include these by default — if your server runs silent or dies on a panic, it's because you haven't added them yet.
The running example: a books API
We won't keep writing throwaway /ping routes. Across this guide we'll grow one real service: a small
books API. The core of it is a single type — a book with an id, a title, and an author:
What just happened: we declared the Book struct the whole guide builds on. Those json:"..."
struct tags tell Echo what to call each field in JSON — so Title becomes "title", not "Title".
Tags work in both directions; binding incoming JSON in Phase 3 leans on them too. Here's the type
returning itself through the now-familiar flow:
e
What just happened: the handler built a Book, and return c.JSON(...) serialized it using those
tags — no map needed when you already have a struct. Hit it and you get clean JSON back:
$ curl localhost:1323/books/sample
{"id":1,"title":"The Go Programming Language","author":"Donovan & Kernighan"}
By the end of the guide this grows into full create/read/update/delete over a real collection of books,
with centralized error handling and tests. For now you've met the cast: an instance, a route, a
handler that returns an error, a context, and the Book we'll spend the next phases turning
into a proper REST API. Next up: routing — path params, query params, and grouping routes so they don't
sprawl.
Recap
- Echo is a fast Go web framework over
net/http— a close peer of Gin. It does the repetitive parts (routing, JSON, middleware) without hiding the standard library underneath. - The mental model is three things: the instance (
*echo.Echo, fromecho.New()) holds your routes; the context (echo.Context, an interface) handles each request; the handler returns an error —func(c echo.Context) error. - Echo's signature trait is the error-returning handler. You
return c.JSON(...)on success orreturn echo.NewHTTPError(...)on failure, and a central handler renders it. ⚠️ Forgetting thereturnis the classic Echo bug. - A first server is tiny:
echo.New()makes the instance,e.GET(path, handler)registers a route,c.JSON(http.StatusOK, ...)writes the response, ande.Start(":1323")listens. Run withgo run main.go, test withcurl. - Unlike Gin, Echo includes no middleware by default. Add
middleware.Logger()andmiddleware.Recover()yourself (fromgithub.com/labstack/echo/v4/middleware) for request logging and crash protection. - The throughline: instance → route → handler → context → response, with errors flowing to a central handler. We'll grow one books API along that path for the rest of the guide.
Quick check
Three questions on the ideas that have to stick — what Echo is, the instance/context split, and the error-returning handler:
[
{
"q": "What is Echo's signature difference from Gin in how handlers work?",
"choices": [
"An Echo handler returns an error (func(c echo.Context) error), and a central handler renders it; a Gin handler returns nothing and writes errors by hand",
"Echo handlers take no arguments at all",
"Echo handlers must return a string that becomes the response body",
"Echo handlers run in a separate goroutine automatically"
],
"answer": 0,
"explain": "Echo handlers are func(c echo.Context) error. You return c.JSON(...) on success or return an error on failure, and Echo's central error handler turns errors into responses. Gin's func(c *gin.Context) has no return value, so you write error responses yourself."
},
{
"q": "What does echo.New() give you that gin.Default() includes but echo.New() does not?",
"choices": [
"Nothing extra — echo.New() returns a bare instance, so you add Logger and Recover middleware yourself with e.Use(...)",
"A built-in database connection",
"Automatic HTTPS certificates",
"Logger and Recover middleware, already wired up like gin.Default()"
],
"answer": 0,
"explain": "echo.New() returns a bare *echo.Echo with no middleware. Unlike gin.Default() (which ships Logger + Recovery), in Echo you opt in: e.Use(middleware.Logger()) and e.Use(middleware.Recover()) from github.com/labstack/echo/v4/middleware."
},
{
"q": "In the mental model, what are the roles of the instance and the context?",
"choices": [
"The instance (*echo.Echo) holds your routes and is started once; the context (echo.Context) is handed to a handler for each request to read input and write output",
"They are the same object with two names",
"The context holds the routes and the instance handles each request",
"The instance is the JSON serializer and the context is the router"
],
"answer": 0,
"explain": "Instance holds the routes, context handles the request. You create one *echo.Echo, register routes on it, and start it; each incoming request gets an echo.Context carrying the request, response writer, params, and helpers. Every handler is func(c echo.Context) error."
}
]
Guide overview · Phase 2: Routing & Groups →
Check your understanding
1. What is Echo's signature difference from Gin in how handlers work?
2. What does echo.New() give you that gin.Default() includes but echo.New() does not?
3. In the mental model, what are the roles of the instance and the context?