This episode dissects Gin middleware: writing your own middleware with c.Next() and c.Abort(), installing it with engine.Use(), using built-in middleware such as Logger, Recovery, BasicAuth, CORS, and gzip, and controlling the order of execution.

Middleware is one of the main reasons people choose Gin. With middleware, you run logic before and after a handler without modifying the handler code itself: logging, authentication, CORS, recovery, compression — all of it can be encapsulated into pieces you can attach to any route.
Episode 6 dissects middleware from two angles: how to write your own custom middleware, and how to take advantage of the most commonly used built-in Gin middleware. Understanding execution order will save you from bugs that are hard to track down.
Middleware is just a HandlerFunc: a function that receives *gin.Context. Two key methods control the flow:
c.Next(): continue execution to the next handler; code after c.Next() runs after the handler finishes.c.Abort(): stop the chain; the next handlers aren't called.func myMiddleware(c *gin.Context) {
// kode sebelum handler
c.Next()
// kode setelah handler selesai
}The code before c.Next() runs pre-processing logic, and the code after it runs post-processing logic. This is what the Logger middleware uses to record request duration.
Let's write a simple logging middleware that records the method, path, and duration:
func requestLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
duration := time.Since(start)
log.Printf("%s %s -> %d (%s)",
c.Request.Method,
c.Request.URL.Path,
c.Writer.Status(),
duration,
)
}
}The function c.Writer.Status() returns the status code the handler wrote. This middleware records the request after the handler finishes because the c.Next() call sits in the middle.
Install middleware on the whole engine or stop the flow with Abort:
r := gin.New()
r.Use(requestLogger())
func requireToken(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "token wajib"})
return
}
c.Set("token", token)
c.Next()
}c.AbortWithStatusJSON(401, ...) stops the chain while writing a JSON response. The function c.Set("token", token) stores a value that the next handler can read with c.Get.
gin.Default() already includes Logger and Recovery. Logger writes an access log for every request; Recovery catches panics in handlers so the server doesn't die:
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())If you don't use gin.Default(), install both explicitly. Recovery is critical in production: an unhandled panic will stop the server process.
For simple protection, Gin provides the BasicAuth middleware:
r.Use(gin.BasicAuth(gin.Accounts{
"arman": "rahasia123",
"dev": "rahasia456",
}))
r.GET("/admin", func(c *gin.Context) {
user := c.MustGet(gin.AuthUserKey).(string)
c.JSON(200, gin.H{"user": user})
})The function c.MustGet(gin.AuthUserKey).(string) retrieves the validated username. This middleware compares credentials in a way that's safe from timing attacks. For more serious systems, episode 13 will cover JWT and sessions.
If your API is called from a browser on a different domain, CORS is required:
go get github.com/gin-contrib/corsimport "github.com/gin-contrib/cors"
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://app.example.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))The AllowOrigins configuration restricts which domains may access your API. Don't use a wildcard * if AllowCredentials is active — that combination is rejected by browsers.
For bandwidth compression, the gin-contrib ecosystem provides a gzip middleware installed with r.Use(gzip.Gzip(gzip.DefaultCompression)). JSON and text responses are compressed automatically when the client sends an Accept-Encoding: gzip header.
The order of Use determines the order of execution. Code after c.Next() runs in reverse order (LIFO):
r.Use(m1) // masuk pertama
r.Use(m2) // masuk kedua
r.GET("/ping", handler)
// alur: m1 -> m2 -> handler -> m2 (sisa) -> m1 (sisa)Pre-processing logic runs from the first registered middleware to the last, while post-processing logic runs in reverse. A real-world example: put Recovery outermost so it can catch panics from middleware inside it.
Not all middleware needs to be global. Install middleware on specific groups:
api := r.Group("/api")
api.Use(requestLogger())
private := api.Group("/private")
private.Use(requireToken)
private.GET("/profile", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "profil dilindungi"})
})The route /api/private/profile inherits requestLogger from the api group and requireToken from the private group. This hierarchy keeps middleware scoped appropriately.
Key takeaways:
HandlerFunc: c.Next() continues, c.Abort() stops.Next() logic runs after the handler finishes.gin.Default() = Logger + Recovery; install them explicitly if using gin.New().Use determines execution order; post-processing runs LIFO.In the next episode, episode 7, we'll dissect response rendering & static files — returning JSON, XML, YAML, TOML, and ProtoBuf, rendering HTML templates, redirects, static files, file uploads, and streaming responses.