What is Gin?

The HTTP router behind every Grit API — routes, context, middleware.

8 mineasy

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

HTTP request │ ▼ ┌─────────────────────────────────────────────┐ │ Gin router → finds handler for URL │ └─────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Global middleware (logger, CORS, recovery)│ └─────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Group middleware (e.g., Auth on /api/*) │ └─────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ YOUR HANDLER │ │ - reads input via c.Param / c.Query / c... │ │ - calls service │ │ - writes response via c.JSON │ └─────────────────────────────────────────────┘ │ ▼ HTTP response
Gin matches the URL to a handler, runs middleware in order, then your handler, then writes the response.

The smallest possible Gin server

package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default() // 1. new router with sensible defaults
r.GET("/ping", func(c *gin.Context) { // 2. register a route
c.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.GET registers a handler for a method+path. There's also POST, PATCH, DELETE, etc.
  • c *gin.Context is 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

apps/api/internal/routes/routes.go (simplified)
func Register(r *gin.Engine, s *Services) {
// Health (public)
r.GET("/healthz", s.HealthHandler.Check)
// All API endpoints under /api
api := r.Group("/api")
// Public auth endpoints
api.POST("/auth/register", s.AuthHandler.Register)
api.POST("/auth/login", s.AuthHandler.Login)
// Everything below requires an auth token
authed := 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 with c.Param("id") inside the handler.

Reading input — the c.* family

func (h *UserHandler) Get(c *gin.Context) {
id := c.Param("id") // /users/:id
q := c.Query("expand") // ?expand=profile
page := c.DefaultQuery("page", "1") // ?page= (default "1")
var input UpdateUserInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(400, gin.H{"error": err.Error()}) // body parse failed
return
}
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 envelope
c.JSON(200, gin.H{"data": user, "message": "ok"})
// Error — stops middleware chain AND writes
c.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.

Where this file lives in a Grit project. Middleware sits at 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.

Open the playground

Quick check

Inside a handler, which Gin method reads the JSON request body into a struct?

Try it

Wire your first real endpoint by hand:

  1. In a fresh Grit project, open apps/api/internal/routes/routes.go.
  2. Add a public route: GET /api/ping → returns {"data": "pong"}.
  3. Add a public route: GET /api/echo/:msg → returns {"data": msg} using c.Param.
  4. Add an AUTHED route: GET /api/me/ping → returns {"user_id": c.GetUint("user_id")} — inside the authed group.
  5. 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