Go HTTP Frameworks: Routers vs Full Frameworks
Go's standard library already ships a capable HTTP server.
Third-party libraries split into routers that compose with net/http and full frameworks that add request context wrappers, binding, and batteries-included middleware.
Knowing which camp a library belongs to tells you how portable your handlers are and how much framework API leaks into business logic.
Summary
- Routers (chi, gorilla/mux) enhance matching and middleware on top of
http.Handler; full frameworks (Gin, Echo) replace the handler signature with a framework-specific context type. - Insight: The split determines testability, migration cost, middleware reuse, and how tightly your handlers couple to a vendor API.
- Key Concepts:
http.Handler, middleware chain, route matching, context wrapper, binding/validation, stdlib compatibility. - When to Use: Picking a library for a new service, auditing handler portability, or deciding whether to stay on stdlib
ServeMux(Go 1.22+) with light wrappers. - Limitations/Trade-offs: Routers stay minimal but you assemble JSON, validation, and logging yourself; full frameworks speed CRUD APIs but increase lock-in and abstraction surface.
- Related Topics:
net/httpservers and middleware, JSON serialization, handler testing withhttptest, and adapter patterns for migration.
Foundations
Every Go HTTP server ultimately calls func(w http.ResponseWriter, r *http.Request).
The standard library defines that contract in http.Handler.
Libraries that respect it are stdlib-compatible: your route functions are plain handlers or http.HandlerFunc, and middleware is typically func(http.Handler) http.Handler.
Routers add a route tree and parameter extraction without changing the handler signature.
chi registers patterns like /users/{id} and exposes chi.URLParam(r, "id").
gorilla/mux adds regex constraints, host matching, and method-scoped subrouters while still ending in http.Handler.
Full frameworks introduce a context wrapper - *gin.Context or echo.Context - that embeds or mirrors http.Request and http.ResponseWriter plus helpers (BindJSON, JSON, Param, Query).
Handlers become func(c *gin.Context) or func(c echo.Context) error.
That wrapper is ergonomic for JSON APIs but is not http.Handler, so you need adapters to mix framework routes with stdlib middleware or mount on foreign routers.
Client request
|
v
net/http Server
|
+-- Router (chi/mux): middleware(http.Handler) -> route handler(w,r)
|
+-- Framework (Gin/Echo): global middleware -> framework context -> handler(c)
Mechanics & Interactions
Middleware runs as nested handlers.
On chi, r.Use(logging) wraps every matched route; outer middleware sees the request first inbound and last outbound.
Gin and Echo register middleware against the engine with framework-specific Next() semantics, but the idea is the same: cross-cutting concerns before the route handler.
Route matching differs in expressiveness and performance.
chi uses a radix tree tuned for common REST paths.
gorilla/mux compiles richer patterns (regex, headers, hosts) at registration time.
Gin and Echo embed routers (Gin uses httprouter-derived matching; Echo has its own tree) optimized for API-style paths.
None of this replaces the Go scheduler or GC; differences show up in allocation per request and how much reflection binding costs.
// Router style: stdlib handler throughout
r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
// ...
})
// Framework style: context wrapper
func getUser(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"id": id})
}Binding and rendering are where full frameworks pull ahead for JSON services.
Gin and Echo decode JSON/query/path into structs with tags and run validation hooks.
Routers leave encoding to encoding/json, third-party validators, or your own DTO layer - more boilerplate, clearer boundaries.
Advanced Considerations & Applications
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
stdlib ServeMux (1.22+) | Zero deps, stable patterns | Fewer grouping and param helpers | Small services, embedded admin APIs |
| chi / mux | Portable handlers, explicit control | Manual JSON/validation wiring | Libraries, gateways, teams standardizing on http.Handler |
| Gin | Fast JSON CRUD, large ecosystem | gin.Context lock-in, global mode footguns | High-throughput REST APIs |
| Echo | Balanced middleware, WebSocket helpers | Smaller community than Gin | APIs needing WS and structured middleware |
Testing stays simplest with http.Handler: httptest.NewRecorder and httptest.NewRequest exercise handlers without booting a framework engine.
Framework handlers need router/engine setup or test helpers that construct a context.
Observability should live in middleware either way.
OpenTelemetry HTTP instrumentation often expects http.Handler; adapters exist for Gin and Echo but add a translation layer.
Migration between routers is cheap if handlers are http.HandlerFunc and path params are read through a thin helper.
Moving off Gin/Echo means rewriting handlers or maintaining adapter shims per route.
Common Misconceptions
- "You need a framework to build production APIs in Go" -
net/httpplus chi or mux is production-grade; frameworks mainly reduce repetitive JSON and routing ceremony. - "Gin is faster because it bypasses the standard library" - It still sits on
net/http; gains come from routing data structures and reduced allocations in hot paths, not magic outside the HTTP stack. - "Middleware written for chi works unchanged on Gin" - Signature differs; you wrap or reimplement per framework unless everything is expressed as
http.Handlerat the edge. - "gorilla/mux is deprecated so routers are dead" - mux is in maintenance mode, but chi and stdlib
ServeMuxcover most new router needs; mux remains valid for regex/host rules. - "Framework context replaces
context.Context" - Request-scoped cancellation and deadlines still flow throughr.Context(); framework wrappers should forward that, not replace it.
FAQs
What is the practical difference between a router and a full framework?
A router matches URLs and chains http.Handler middleware.
A full framework changes the handler signature and bundles JSON binding, validation, and response helpers on a custom context type.
Can I use chi with Gin on the same server?
Not cleanly on one route tree.
Mount one as a sub-http.Handler on the other (r.Mount("/gin", ginEngine)) and keep path prefixes explicit.
Is stdlib ServeMux enough now that Go 1.22 added method and path patterns?
Often yes for small services.
Reach for chi when you want route groups, middleware stacks, or cleaner param access without a framework context.
Why do teams pick Gin for JSON APIs?
Binding tags, grouped routes, and built-in JSON rendering reduce boilerplate for CRUD endpoints at the cost of gin.Context coupling.
When does gorilla/mux still win over chi?
When you need regex path constraints, host-based routing, or header/query matchers declared declaratively on the route.
Do full frameworks prevent using standard middleware?
They prefer native middleware, but many apps wrap stdlib middleware with adapter functions at the engine edge.
How does handler portability relate to hexagonal architecture?
Keep business logic in plain functions/services; let the framework context stop at the handler boundary so core code stays router-agnostic.
Are WebSockets a reason to choose Echo?
Echo documents first-class WebSocket helpers; chi and mux work with golang.org/x/net/websocket or third-party packages with more wiring.
Does framework choice affect horizontal scaling?
Not directly - scaling is about statelessness, DB pools, and deployment.
Framework overhead is usually noise compared to I/O and serialization.
Should new Go services default to a framework?
Default to stdlib or chi until JSON binding volume justifies Gin/Echo; avoid choosing a framework for routing alone.
How do I compare performance fairly?
Benchmark your real handlers with httptest or load tools; micro-benchmarks of hello-world routes rarely predict API workloads dominated by DB and JSON size.
Where does validation live in router-first apps?
Decode with json.Decoder, validate with go-playground/validator or hand-rolled checks in a DTO package - explicit, but under your control.
Related
- Web Frameworks Basics - hello-world routes in chi and Gin
- chi: Lightweight Routing & Middleware - stdlib-compatible routing
- Gin: Fast JSON APIs & Binding - framework context and binding
- Framework Selection Decision Guide - ranked choices by scenario
- Middleware & Decorator Patterns -
http.Handlerchaining model - net/http: Go's Batteries-Included HTTP Stack - stdlib foundation
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).