Go Web Frameworks: net/http, Chi, Gin, Echo and Fiber · The Standard Library and the Big Four
Gin: The Most Popular Go Web Framework
Gin gives you a fast router, a Context with helpers for JSON, binding and validation of request bodies with struct tags, route groups and middleware. Build the books API the Gin way.
What you'll build
The books API with Gin: a validated POST /books that rejects bad input with a helpful message, a versioned route group, and a middleware that adds a request ID.
The idea
Gin (github.com/gin-gonic/gin) is the most widely used Go web framework. Handlers receive a *gin.Context that wraps the request and response and adds helpers:
c.Param("id"),c.Query("page"),c.GetHeader(...),c.JSON(status, value),c.AbortWithStatusJSON(...),- binding:
c.ShouldBindJSON(&input)decodes the body and validates it usingbinding:"required,min=1"struct tags (powered by go-playground/validator), c.Set/c.Getto pass values from middleware to handlers,c.Next()/c.Abort()to control the chain.
gin.Default() includes a logger and panic recovery; gin.New() starts empty so you choose middleware. In production set release mode (GIN_MODE=release or gin.SetMode(gin.ReleaseMode)).
A *gin.Engine is an http.Handler, so it runs with http.Server and tests with httptest.
The code
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"github.com/gin-gonic/gin"
)
type CreateBook struct {
Title string `json:"title" binding:"required,min=2"`
Author string `json:"author" binding:"required"`
Year int `json:"year" binding:"omitempty,gte=1450,lte=2100"`
}
func requestID() gin.HandlerFunc {
n := 0
return func(c *gin.Context) {
n++
c.Set("requestID", fmt.Sprintf("req-%d", n)) // available to every later handler
c.Header("X-Request-ID", fmt.Sprintf("req-%d", n))
c.Next()
}
}
func main() {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Recovery(), requestID())
books := []CreateBook{{Title: "Learning Go", Author: "Jon Bodner", Year: 2024}}
v1 := r.Group("/api/v1")
{
v1.GET("/books/:id", func(c *gin.Context) {
if c.Param("id") != "1" {
c.JSON(http.StatusNotFound, gin.H{"error": "book not found"})
return
}
c.JSON(http.StatusOK, books[0])
})
v1.POST("/books", func(c *gin.Context) {
var in CreateBook
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
books = append(books, in)
c.JSON(http.StatusCreated, gin.H{"id": len(books), "request": c.GetString("requestID")})
})
}
send := func(method, path, body string) {
req := httptest.NewRequest(method, path, strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
out, _ := io.ReadAll(rec.Body)
fmt.Printf("%s %s -> %d %s %s\n", method, path, rec.Code, rec.Header().Get("X-Request-ID"), out)
}
send("GET", "/api/v1/books/1", "")
send("POST", "/api/v1/books", `{"title":"Concurrency in Go","author":"Katherine Cox-Buday","year":2017}`)
send("POST", "/api/v1/books", `{"title":"X"}`)
send("POST", "/api/v1/books", `{"title":"Old Book","author":"Anon","year":1200}`)
}
Output
GET /api/v1/books/1 -> 200 req-1 {"title":"Learning Go","author":"Jon Bodner","year":2024}
POST /api/v1/books -> 201 req-2 {"id":2,"request":"req-2"}
POST /api/v1/books -> 422 req-3 {"error":"Key: 'CreateBook.Title' Error:Field validation for 'Title' failed on the 'min' tag\nKey: 'CreateBook.Author' Error:Field validation for 'Author' failed on the 'required' tag"}
POST /api/v1/books -> 422 req-4 {"error":"Key: 'CreateBook.Year' Error:Field validation for 'Year' failed on the 'gte' tag"}Run this on your computer — see “Get Started” for setup.
Validation errors come from the validator in a developer-oriented format (Key: 'CreateBook.Author' Error:Field validation for 'Author' failed on the 'required' tag). Real APIs translate them into friendly per-field messages; the error-handling lesson shows how.
Common mistakes
c.Bind…vsc.ShouldBind….BindJSONwrites a 400 response itself on failure;ShouldBindJSONreturns the error so you decide the response. PreferShouldBind….- Forgetting
returnafterc.JSONfor an error. Gin doesn't stop your function;Abortonly stops later middleware. - Using
gin.Default()blindly in production. Its logger writes plain text to stdout; many teams swap it for structured logging. - Leaking the raw validator message to users. Map it to clear field errors.
- Copying
*gin.Contextinto goroutines. Usec.Copy()if work continues after the handler returns.
Key points
- Gin: fast router +
*gin.Contexthelpers + binding/validation via struct tags. ShouldBindJSON+binding:"required,…"validates input in one call.- Groups (
r.Group("/api/v1")) and middleware (c.Set,c.Next,c.Abort). - The engine is an
http.Handler: run it withhttp.Server, test it withhttptest.
Further reading: Gin documentation, gin on pkg.go.dev.
Practice questions
What's the difference between c.BindJSON and c.ShouldBindJSON in Gin?
Answer
Both decode and validate the body. BindJSON automatically aborts with a 400 response on failure; ShouldBindJSON only returns the error, letting you choose the status code and error format. ShouldBind is usually preferred.
How do Gin middleware pass data to handlers?
Answer
With c.Set(key, value) in the middleware and c.Get / c.GetString(key) in later handlers. c.Next() runs the rest of the chain; c.Abort() stops later handlers from running.
Open in the app to mark lessons complete, take notes and track your progress.