Go with the Gin framework is one of the fastest, cleanest ways to build REST APIs in 2026 โ compiled performance, simple syntax, and a mature ecosystem. This guide builds a complete API with routing, validation, database access, and middleware.
๐ Table of Contents
Why Go and Gin?
- Performance: Compiled Go handles high concurrency with minimal resources
- Simple deployment: A single static binary โ no runtime to install
- Gin’s speed: One of the fastest Go web frameworks, with a clean API
- Great for microservices: Small binaries, fast startup, low memory
Setup
mkdir go-api && cd go-api
go mod init github.com/you/go-api
go get github.com/gin-gonic/gin
go get gorm.io/gorm gorm.io/driver/postgres
Basic Server and Routing
// main.go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default() // includes logger and recovery middleware
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
// Route groups
api := r.Group("/api/v1")
{
api.GET("/users", getUsers)
api.GET("/users/:id", getUser)
api.POST("/users", createUser)
api.PUT("/users/:id", updateUser)
api.DELETE("/users/:id", deleteUser)
}
r.Run(":8080")
}
Models and Database
// models.go
package main
import "gorm.io/gorm"
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email" gorm:"unique"`
}
var db *gorm.DB
func initDB() {
dsn := "host=localhost user=postgres password=pass dbname=mydb port=5432"
var err error
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
db.AutoMigrate(&User{})
}
Handlers with JSON Binding and Validation
// handlers.go
func getUsers(c *gin.Context) {
var users []User
db.Find(&users)
c.JSON(http.StatusOK, users)
}
func getUser(c *gin.Context) {
var user User
if err := db.First(&user, c.Param("id")).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusOK, user)
}
func createUser(c *gin.Context) {
var user User
// Binds JSON and runs validation tags (required, email)
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := db.Create(&user).Error; err != nil {
c.JSON(http.StatusConflict, gin.H{"error": "email already exists"})
return
}
c.JSON(http.StatusCreated, user)
}
Custom Middleware
// Authentication middleware
func authRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
c.Abort() // stop the chain
return
}
userID, err := verifyToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
c.Abort()
return
}
c.Set("userID", userID) // pass to handlers
c.Next()
}
}
// Apply to a route group
protected := r.Group("/api/v1")
protected.Use(authRequired())
Structured Error Handling
// Consistent error responses
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func respondError(c *gin.Context, code int, msg string) {
c.JSON(code, APIError{Code: code, Message: msg})
}
// Recovery middleware (Gin includes one, but you can customize)
r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
respondError(c, http.StatusInternalServerError, "internal server error")
}))
Graceful Shutdown
func main() {
r := setupRouter()
srv := &http.Server{Addr: ":8080", Handler: r}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// Graceful shutdown with 5-second timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)
}
Frequently Asked Questions
Q: Gin vs Echo vs Fiber?
A: All are fast and mature. Gin is the most popular with the largest ecosystem. Echo is similar. Fiber has an Express-like API. For most projects, Gin's popularity and documentation make it the safe default.
Q: GORM or raw SQL / sqlx?
A: GORM is convenient for CRUD and rapid development. For complex queries or maximum control and performance, raw SQL with sqlx or pgx is preferred. Many teams use GORM for basics and drop to raw SQL where needed.
Q: How do I validate request data?
A: Gin uses struct tags with the validator library โ binding:"required,email". ShouldBindJSON runs validation automatically and returns errors you can return to the client.
Q: How do I handle CORS in Gin?
A: Use the gin-contrib/cors middleware: r.Use(cors.Default()) for permissive dev, or configure specific origins, methods, and headers for production.
Q: How do I test Gin handlers?
A: Use httptest to create a test request and recorder, then assert on the response. Gin's router works with Go's standard testing tools for clean, fast handler tests.
Conclusion
Go with Gin delivers fast, clean REST APIs that compile to a single deployable binary. This guide covers the essentials โ route groups, JSON binding with validation, GORM database access, custom middleware for auth, structured errors, and graceful shutdown. Go's performance and simple deployment make it excellent for microservices and high-concurrency APIs. Add rate limiting, structured logging, and OpenAPI docs before production, and you have a robust, scalable backend.
๐ You might also like
๐ Share this article



โ๏ธ Leave a Comment