What is Gin?
The HTTP router behind every Grit API — routes, context, middleware.
Every HTTP request that hits a Grit API passes through Gin. Before you can write or change a route, you need a feel for what Gin is, what it does for you, and what you have to do yourself.
If you're new to Go syntax (structs, methods, slices, error handling), skim the Go primer first — this lesson assumes you can read it.
What is Gin?
Gin is a small, fast HTTP web framework for Go. It does three jobs:
- Routes URLs to functions you write (
GET /api/users → ListUsers). - Parses the request (path params, query, body, headers) into Go values.
- Serializes your response (struct → JSON, with the right status code).
Without Gin you would write net/http by hand and parse JSON manually for every endpoint. Gin removes 90% of that ceremony while staying thin enough to read in an afternoon.
The request lifecycle
A Gin request
The smallest possible Gin server
package mainimport "github.com/gin-gonic/gin"func main() {r := gin.Default() // 1. new router with sensible defaultsr.GET("/ping", func(c *gin.Context) { // 2. register a routec.JSON(200, gin.H{"message": "pong"}) // 3. write response})r.Run(":8080") // 4. start listening}
Four lines worth understanding:
gin.Default()gives you a router with the logger and panic-recovery middleware pre-attached. Grit uses this.r.GETregisters a handler for a method+path. There's alsoPOST,PATCH,DELETE, etc.c *gin.Contextis the most important type in Gin. It bundles request + response + middleware data. Every handler gets one.c.JSON(status, body)serializes the body as JSON and sets the status code. Done.
How a Grit project wires it up
func Register(r *gin.Engine, s *Services) {// Health (public)r.GET("/healthz", s.HealthHandler.Check)// All API endpoints under /apiapi := r.Group("/api")// Public auth endpointsapi.POST("/auth/register", s.AuthHandler.Register)api.POST("/auth/login", s.AuthHandler.Login)// Everything below requires an auth tokenauthed := api.Group("")authed.Use(middleware.RequireAuth(s.JWT))authed.GET("/users", s.UserHandler.List)authed.GET("/users/:id", s.UserHandler.Get)authed.PATCH("/users/:id", s.UserHandler.Update)}
Three things to notice:
- Groups.
r.Group("/api")factors out a common prefix. Cleaner than typing it on every line. - Middleware on groups.
authed.Use(middleware.RequireAuth(...))applies the auth gate to ONLY routes registered after that call. Public ones above stay open. - Path params with
:id. Gin parses these for you — read withc.Param("id")inside the handler.
Reading input — the c.* family
func (h *UserHandler) Get(c *gin.Context) {id := c.Param("id") // /users/:idq := c.Query("expand") // ?expand=profilepage := c.DefaultQuery("page", "1") // ?page= (default "1")var input UpdateUserInputif err := c.ShouldBindJSON(&input); err != nil {c.JSON(400, gin.H{"error": err.Error()}) // body parse failedreturn}authedID := c.GetUint("user_id") // set by RequireAuth middleware}
Five lookups, five different sources. Memorise these — you'll type them dozens of times:
c.Param— URL placeholder (:id).c.Query— querystring (?key=v).c.ShouldBindJSON— JSON request body → struct.c.GetUint / GetString— value set by middleware.c.GetHeader— HTTP header.
Writing output — c.JSON, c.AbortWithStatusJSON
// Success — standard Grit envelopec.JSON(200, gin.H{"data": user, "message": "ok"})// Error — stops middleware chain AND writesc.AbortWithStatusJSON(404, gin.H{"error": gin.H{"code": "not_found","message": "User not found",}})
Use Abort when something has gone wrong — it stops any downstream middleware from running. c.JSON alone keeps the chain going (rarely what you want for errors).
Middleware in 10 lines
func RequireAuth(jwtSvc *JWTService) gin.HandlerFunc {return func(c *gin.Context) {token := c.GetHeader("Authorization")if token == "" {c.AbortWithStatusJSON(401, gin.H{"error": "missing token"})return}claims, err := jwtSvc.Verify(strings.TrimPrefix(token, "Bearer "))if err != nil {c.AbortWithStatusJSON(401, gin.H{"error": "bad token"})return}c.Set("user_id", claims.UserID) // available downstream via c.GetUint("user_id")c.Next() // continue to the handler}}
A middleware IS just a handler that calls c.Next() when it wants the chain to continue. Authentication, rate limiting, request logging, request ID — all the same shape.
apps/api/internal/middleware/. Routes at apps/api/internal/routes/routes.go. Handlers at apps/api/internal/handlers/. We'll walk all of them in the next lesson.Playground challenge
Add a hello endpoint
In the playground, scaffold a minimal Gin server. Add a GET /hello/:name that returns {"greeting": "hi NAME"} where NAME is the path param. No DB, no auth — just routing and a JSON response. Should take 5 minutes.
Quick check
Try it
Wire your first real endpoint by hand:
- In a fresh Grit project, open
apps/api/internal/routes/routes.go. - Add a public route:
GET /api/ping → returns {"data": "pong"}. - Add a public route:
GET /api/echo/:msg → returns {"data": msg}usingc.Param. - Add an AUTHED route:
GET /api/me/ping → returns {"user_id": c.GetUint("user_id")}— inside theauthedgroup. - Test all three with curl. The authed one should return 401 without a token and 200 with one.
What's next
Next lesson — GORM. The ORM that turns Go structs into SQL. Once Gin gets the request to a handler and the handler calls a service, the service uses GORM to actually read/write the database.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub