Web Frameworks Basics
10 examples to get you started with Web Frameworks - 7 basic and 3 intermediate.
Search across all documentation pages
10 examples to get you started with Web Frameworks - 7 basic and 3 intermediate.
mkdir webdemo && cd webdemo && go mod init example.com/webdemo.go get github.com/go-chi/chi/v5 and go get github.com/gin-gonic/gin.go run . after saving as main.go (or split files in the same module).The baseline every framework builds on: http.HandlerFunc.
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello, stdlib")
})
http.ListenAndServe(":8080", nil)
}http.HandleFunc registers on DefaultServeMux.ResponseWriter and Request - the universal contract.http.Server in production; this shows the smallest shape.Related: net/http: Go's Batteries-Included HTTP Stack - stdlib HTTP model
chi stays compatible with http.Handler while adding path params.
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
http.ListenAndServe(":8080", r)
}chi.NewRouter() returns an http.Handler you pass to ListenAndServe.Get, Post) set HTTP verb constraints.Related: chi: Lightweight Routing & Middleware - route groups and middleware
Named segments appear in the pattern and are read from the request.
r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
w.Write([]byte("user " + id))
}){id} binds one path segment; use {id:*} for greedy rest paths when needed.chi.URLParam returns empty string if the key is missing - validate before use.Related: Go HTTP Frameworks: Routers vs Full Frameworks - router vs framework model
Use wraps all routes registered after it on that router.
import "github.com/go-chi/chi/v5/middleware"
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Get("/time", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})Use is outermost on the way in.func(http.Handler) http.Handler.Related: Middleware & Decorator Patterns - handler wrapping model
Mount related routes under a shared path and middleware.
r.Route("/api/v1", func(r chi.Router) {
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"status":"ok"}`))
})
r.Get("/version", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"version":"1"}`))
})
})Route creates a sub-router inheriting parent middleware unless you branch earlier./api/v1/health matches the health handler.Gin uses *gin.Context instead of raw http.ResponseWriter.
package main
import "github.com/gin-gonic/gin"
func main() {
g := gin.Default() // Logger + Recovery middleware
g.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
g.Run(":8080")
}gin.Default() enables debug logging middleware - use gin.New() plus explicit middleware in production tuning.c.JSON sets Content-Type and encodes the map.gin.H is a map[string]any shorthand for small payloads.Related: Gin: Fast JSON APIs & Binding - binding and validation
Framework context exposes params with Param.
g.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"id": id})
}):id syntax; chi uses {id} - migration requires path rewrites or adapters.strconv or binding helpers.*path with slightly different semantics than chi's {path:*}.Bind request bodies into structs with tags.
type createUser struct {
Email string `json:"email" binding:"required,email"`
}
g.POST("/users", func(c *gin.Context) {
var in createUser
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(201, gin.H{"email": in.Email})
})ShouldBindJSON avoids double-writing responses on failure (prefer over BindJSON in handlers).github.com/go-playground/validator wired through Gin.api or transport package separate from domain models.Mount independent routers for modular services.
admin := chi.NewRouter()
admin.Get("/stats", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("stats"))
})
r.Mount("/admin", admin)Mount strips the prefix before the sub-router matches.Write logic once as http.HandlerFunc, register on chi; Gin needs a thin adapter.
func showHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"ok":true}`))
}
// chi
r.Get("/health", showHealth)
// gin adapter
g.GET("/health", func(c *gin.Context) {
showHealth(c.Writer, c.Request)
})c.Writer and c.Request are the underlying stdlib types.chi.Router under Gin or vice versa instead of per-route adapters.Related: Migrating Between Routers Without Rewriting Handlers - adapter patterns
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).
Reviewed by Chris St. John·Last updated Jul 18, 2026