Gin is Go's most popular web framework, offering a martini-like API with up to 40x better performance. Its balance of simplicity, performance, and rich middleware ecosystem makes it the go-to choice for building REST APIs in Go. This guide covers building a production-ready API from project setup through deployment.
Project Setup
mkdir gin-api && cd gin-api
go mod init github.com/yourname/gin-api
go get github.com/gin-gonic/gin
go get github.com/gin-contrib/cors
go get github.com/gin-contrib/gzip
go get github.com/joho/godotenv
Application Code
// main.go
package main
import (
"log"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/gin-contrib/cors"
"github.com/gin-contrib/gzip"
"github.com/joho/godotenv"
)
func main() {
godotenv.Load()
if os.Getenv("GIN_MODE") == "release" {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
r.Use(gin.Recovery())
r.Use(gin.Logger())
r.Use(gzip.Gzip(gzip.DefaultCompression))
r.Use(cors.Default())
// Health check
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "healthy"})
})
// API v1 group
v1 := r.Group("/api/v1")
{
users := v1.Group("/users")
{
users.GET("", listUsers)
users.GET("/:id", getUser)
users.POST("", createUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
}
}
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("Starting server on :%s", port)
r.Run(":" + port)
}
// Request/Response types
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=2,max=100"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"required,gte=1,lte=150"`
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
type ErrorResponse struct {
Error string `json:"error"`
Details map[string]string `json:"details,omitempty"`
}
func listUsers(c *gin.Context) {
// Pagination
page := c.DefaultQuery("page", "1")
limit := c.DefaultQuery("limit", "20")
_ = page; _ = limit
users := []User{
{ID: 1, Name: "Alice", Email: "alice@example.com", Age: 30},
{ID: 2, Name: "Bob", Email: "bob@example.com", Age: 25},
}
c.JSON(http.StatusOK, gin.H{
"data": users,
"total": len(users),
"page": 1,
})
}
func getUser(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, User{ID: 1, Name: "Alice", Email: "alice@example.com"})
_ = id
}
func createUser(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
user := User{ID: 3, Name: req.Name, Email: req.Email, Age: req.Age}
c.JSON(http.StatusCreated, user)
}
func updateUser(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "updated"})
}
func deleteUser(c *gin.Context) {
c.Status(http.StatusNoContent)
}
Custom Middleware
// middleware/auth.go
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" || !strings.HasPrefix(token, "Bearer ") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Authorization header required",
})
return
}
tokenStr := strings.TrimPrefix(token, "Bearer ")
// Validate token...
userID, err := validateToken(tokenStr)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Invalid token",
})
return
}
c.Set("userID", userID)
c.Next()
}
}
func RateLimit(limit int) gin.HandlerFunc {
// Simple in-memory rate limiter
return func(c *gin.Context) {
ip := c.ClientIP()
// Check rate limit for IP...
_ = ip; _ = limit
c.Next()
}
}
Structured Error Handling
// errors/errors.go
package errors
import (
"net/http"
"github.com/gin-gonic/gin"
)
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (e *AppError) Error() string {
return e.Message
}
func NotFound(msg string) *AppError {
return &AppError{Code: http.StatusNotFound, Message: msg}
}
func BadRequest(msg string) *AppError {
return &AppError{Code: http.StatusBadRequest, Message: msg}
}
// Error handling middleware
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
err := c.Errors.Last().Err
if appErr, ok := err.(*AppError); ok {
c.JSON(appErr.Code, gin.H{"error": appErr.Message})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
}
}
}
}
Database with GORM
go get gorm.io/gorm
go get gorm.io/driver/postgres
// database/db.go
package database
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
"log"
"os"
)
var DB *gorm.DB
func Init() {
dsn := os.Getenv("DATABASE_URL")
var err error
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("Database connection failed: %v", err)
}
sqlDB, _ := DB.DB()
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(50)
// Auto-migrate models
DB.AutoMigrate(&User{})
}
Build and Deploy
# Build for Linux
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w -X main.version=1.0.0" -o api-server .
# Docker deployment
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /server .
FROM scratch
COPY --from=builder /server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]
Testing
// handlers_test.go
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestHealthEndpoint(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "healthy"})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/health", nil)
r.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "healthy")
}
Summary
Gin is the standard choice for Go web APIs, offering excellent performance with a clean, familiar API. Its validation, middleware, and routing capabilities handle most web application needs out of the box. Deployment follows the standard Go pattern: compile a single binary, deploy with systemd, and reverse proxy with Nginx. The result is a fast, memory-efficient API server that typically handles 50,000-100,000 requests per second on modest VPS hardware.