Web Frameworks Best Practices
A condensed summary of the 25 most important web framework practices drawn from every page in this section.
-
Keep handlers thin: Decode transport input, call services, map errors to HTTP - business rules live outside framework context types.
-
Prefer
http.Handlerat the core: Write portable handlers when possible so chi, mux, and framework adapters stay interchangeable. -
Choose routers before frameworks: Reach for stdlib or chi until JSON binding volume justifies Gin or Echo lock-in.
-
Configure
http.Servertimeouts: Router middleware is not a substitute forReadHeaderTimeout,ReadTimeout, andWriteTimeouton the server. -
Register recovery middleware: Panic recovery belongs in the outer middleware stack on every public listener.
-
Order middleware deliberately: Logging and tracing outermost, auth next, timeout and body limits before handlers that may block.
-
Use route groups for API versions: Prefix
/api/v1with group-level middleware instead of repeating paths per handler. -
Parse path params safely: URL params are strings; validate with
strconvor binding before domain lookups. -
Prefer
ShouldBind*in Gin: AvoidBindJSONwhen you need custom error JSON -ShouldBindJSONdoes not write responses automatically. -
Wire validators explicitly in Echo: Set
e.Validatorbefore relying onc.Validate- validation is not automatic out of the box. -
Return errors centrally in Echo: Use handler
errorreturns and a customHTTPErrorHandlerinstead of scattering status writes. -
Set Gin release mode in production: Use
gin.New()with chosen middleware andgin.SetMode(gin.ReleaseMode)to avoid debug overhead. -
Limit request bodies: Apply
http.MaxBytesReaderor framework equivalents before decoding large JSON payloads. -
Propagate
r.Context(): Pass request context into database and RPC calls for cancellation and deadlines. -
Use typed context keys: Avoid string context keys for request-scoped values; use unexported custom types to prevent collisions.
-
Document host and proxy behavior: When using mux host matchers or subdomain routing, normalize
Host/X-Forwarded-Hostbehind load balancers. -
Mount migrations with prefixes: Run old and new routers side by side (
/v1,/v2) until metrics show clean cutover. -
Avoid double middleware: When mounting routers, apply logging and auth once at the outermost handler.
-
Test portable handlers with
httptest: Exercisehttp.HandlerFuncdirectly without booting framework engines for unit tests. -
Map validation errors for clients: Translate validator failures into field-scoped problem JSON instead of raw
err.Error()strings. -
Use DTO structs at the edge: Keep transport structs separate from domain models to prevent JSON tags leaking inward.
-
Pick mux only for matchers: Choose gorilla/mux when host/regex/header rules are required, not for ordinary CRUD convenience.
-
Authenticate WebSocket upgrades early: Validate credentials during the upgrade request before switching protocols.
-
Record framework choice in ADRs: Document scenario, ranked alternatives, and migration adapters when standardizing on a library.
-
Keep observability stdlib-shaped: Prefer
http.HandlerOTel middleware or verified framework contrib adapters for consistent traces.
FAQs
Should every Go service use a framework?
No - stdlib plus chi covers many production APIs; frameworks mainly reduce JSON and routing ceremony.
What is the single highest-leverage habit?
Thin handlers that call plain Go services - it improves tests and eases router migration.
How do I enforce these in review?
Checklist: server timeouts, recovery middleware, context propagation, and no business logic in gin.Context beyond binding.
When should I revisit framework choice?
When adding WebSockets, multi-tenant hosts, or OpenAPI codegen - requirements may outgrow the original pick.
Do best practices differ for internal vs public APIs?
Public APIs need stricter error mapping, body limits, and TLS termination discipline; internal APIs still need timeouts and context cancellation.
How do these relate to net/http practices?
Frameworks sit on net/http - server configuration, client pooling, and middleware concepts from net/http still apply.
Should platform teams allow multiple frameworks?
Only with shared observability contracts and portable handler guidelines; otherwise standardize on chi or one full framework.
What is the most common migration mistake?
Rewriting working handlers to framework contexts without a strangler mount plan - prefer adapters and prefixes first.
How many middleware layers are reasonable?
As many as you can diagram on one page - if order is unclear, consolidate or document the standard stack in main.
Where do serialization choices fit?
Keep JSON encoding in handlers or small response helpers; share schema types via DTO packages, not global gin.H maps.
Related
- Go HTTP Frameworks: Routers vs Full Frameworks - conceptual overview
- Framework Selection Decision Guide - scenario rankings
- Migrating Between Routers Without Rewriting Handlers - portability patterns
- Middleware & Decorator Patterns - middleware composition
- Production net/http Configuration Checklist - server hardening
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).