Web Applications with Gin
Building web applications with godi using the popular Gin web framework.
The Controller Pattern with Gin
Gin’s middleware system works perfectly with godi’s dependency injection. Use middleware to create scopes and controllers for clean handler logic:
// Controller with dependencies injected
type PostController struct {
postService *PostService
logger *Logger
}
// Use godi.In for dependency injection
type PostControllerParams struct {
godi.In
PostService *PostService
Logger *Logger
}
func NewPostController(params PostControllerParams) *PostController {
return &PostController{
postService: params.PostService,
logger: params.Logger,
}
}
// Gin handlers as controller methods
func (c *PostController) CreatePost(ctx *gin.Context) {
// Access request ID from Gin context
requestID := ctx.GetString("requestID")
c.logger.Info("Creating post", "requestID", requestID)
var req CreatePostRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(400, gin.H{"error": err.Error()})
return
}
post, err := c.postService.CreatePost(req.Title, req.Content)
if err != nil {
c.logger.Error("Failed to create post", "error", err, "requestID", requestID)
ctx.JSON(400, gin.H{"error": err.Error()})
return
}
ctx.JSON(200, post)
}
Setting Up Middleware
Create Gin middleware that sets up a scope for each request. The scope’s context automatically contains itself:
// Middleware creates scope and enriches Gin context
func ScopeMiddleware(provider godi.Provider) gin.HandlerFunc {
return func(c *gin.Context) {
// Generate request ID for tracing
requestID := uuid.New().String()
c.Set("requestID", requestID)
c.Header("X-Request-ID", requestID)
// Create context with request ID
ctx := context.WithValue(c.Request.Context(), "requestID", requestID)
// Create scope with enriched context
scope, err := provider.CreateScope(ctx)
if err != nil {
c.AbortWithStatusJSON(500, gin.H{"error": "Internal error"})
return
}
defer scope.Close()
// Update Gin's request context with the scope's context
// The scope's context contains the scope itself!
c.Request = c.Request.WithContext(scope.Context())
c.Next()
}
}
// Helper to get scope from request context
func GetScope(c *gin.Context) (godi.Scope, error) {
return godi.FromContext(c.Request.Context())
}
Complete Example: Blog API
Let’s build a blog API with Gin and godi.
Step 1: Models and Services
// models.go
type Post struct {
ID string `json:"id"`
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
CreatedAt time.Time `json:"created_at"`
}
type CreatePostRequest struct {
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
}
// services.go
type PostService struct {
db *Database
}
func NewPostService(db *Database) *PostService {
return &PostService{db: db}
}
func (s *PostService) CreatePost(title, content string) (*Post, error) {
post := &Post{
ID: uuid.New().String(),
Title: title,
Content: content,
CreatedAt: time.Now(),
}
return s.db.SavePost(post)
}
func (s *PostService) GetPosts() ([]*Post, error) {
return s.db.GetAllPosts()
}
func (s *PostService) GetPost(id string) (*Post, error) {
return s.db.GetPost(id)
}
Step 2: Request Context Service
// Request-scoped service for request metadata
func NewRequestContext(ctx context.Context) *RequestContext {
requestID, _ := ctx.Value("requestID").(string)
userID, _ := ctx.Value("userID").(string)
return &RequestContext{
RequestID: requestID,
UserID: userID,
StartTime: time.Now(),
}
}
type RequestContext struct {
RequestID string
UserID string
StartTime time.Time
}
Step 3: Controllers
// PostController handles post-related requests
type PostController struct {
postService *PostService
logger *Logger
}
type PostControllerParams struct {
godi.In
PostService *PostService
Logger *Logger
}
func NewPostController(params PostControllerParams) *PostController {
return &PostController{
postService: params.PostService,
logger: params.Logger,
}
}
func (c *PostController) CreatePost(ctx *gin.Context) {
requestID := ctx.GetString("requestID")
c.logger.Info("Creating post", "requestID", requestID)
var req CreatePostRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(400, gin.H{"error": err.Error()})
return
}
post, err := c.postService.CreatePost(req.Title, req.Content)
if err != nil {
c.logger.Error("Failed to create post", "error", err, "requestID", requestID)
ctx.JSON(400, gin.H{"error": err.Error()})
return
}
ctx.JSON(201, post)
}
func (c *PostController) GetPosts(ctx *gin.Context) {
requestID := ctx.GetString("requestID")
c.logger.Info("Getting posts", "requestID", requestID)
posts, err := c.postService.GetPosts()
if err != nil {
ctx.JSON(500, gin.H{"error": err.Error()})
return
}
ctx.JSON(200, gin.H{
"posts": posts,
"request_id": requestID,
})
}
func (c *PostController) GetPost(ctx *gin.Context) {
requestID := ctx.GetString("requestID")
postID := ctx.Param("id")
c.logger.Info("Getting post", "postID", postID, "requestID", requestID)
post, err := c.postService.GetPost(postID)
if err != nil {
ctx.JSON(404, gin.H{"error": "Post not found"})
return
}
ctx.JSON(200, post)
}
// HealthController for health checks
type HealthController struct {
db *Database
logger *Logger
}
type HealthControllerParams struct {
godi.In
DB *Database
Logger *Logger
}
func NewHealthController(params HealthControllerParams) *HealthController {
return &HealthController{
db: params.DB,
logger: params.Logger,
}
}
func (h *HealthController) CheckHealth(ctx *gin.Context) {
requestID := ctx.GetString("requestID")
health := gin.H{
"status": "healthy",
"request_id": requestID,
"timestamp": time.Now(),
"checks": gin.H{
"database": h.db.Ping() == nil,
},
}
ctx.JSON(200, health)
}
Step 4: Wire Everything Together
func main() {
// Set up modules
appModule := godi.NewModule("app",
// Infrastructure (Singleton - shared)
godi.AddSingleton(NewLogger),
godi.AddSingleton(NewDatabase),
// Request-scoped services
godi.AddScoped(NewRequestContext),
godi.AddScoped(NewPostService),
// Controllers (Scoped - per request)
godi.AddScoped(NewPostController),
godi.AddScoped(NewHealthController),
)
// Build provider
collection := godi.NewCollection()
collection.AddModules(appModule)
provider, err := collection.Build()
if err != nil {
log.Fatal(err)
}
defer provider.Close()
// Set up Gin
router := gin.Default()
// Add middleware
router.Use(ScopeMiddleware(provider))
// Register routes
RegisterRoutes(router)
log.Println("Server starting on :8080")
router.Run(":8080")
}
func RegisterRoutes(router *gin.Engine) {
// Helper function to resolve controller from scope
withController := func[T any](handler func(T, *gin.Context)) gin.HandlerFunc {
return func(c *gin.Context) {
scope, ok := godi.FromContext(c.Request.Context())
if !ok {
c.JSON(500, gin.H{"error": "Internal error"})
return
}
controller, err := godi.Resolve[T](scope)
if err != nil {
c.JSON(500, gin.H{"error": "Service error"})
return
}
handler(controller, c)
}
}
// Post routes
posts := router.Group("/posts")
{
posts.POST("", withController((*PostController).CreatePost))
posts.GET("", withController((*PostController).GetPosts))
posts.GET("/:id", withController((*PostController).GetPost))
}
// Health route
router.GET("/health", withController((*HealthController).CheckHealth))
}
Step 5: Test the API
# Create a post
curl -X POST http://localhost:8080/posts \
-H "Content-Type: application/json" \
-d '{"title":"Hello Gin","content":"Using Gin with godi!"}'
# Get all posts
curl http://localhost:8080/posts
# Get specific post
curl http://localhost:8080/posts/{post-id}
# Check health
curl http://localhost:8080/health
Testing with Gin
func TestPostController(t *testing.T) {
// Test module
testModule := godi.NewModule("test",
godi.AddSingleton(func() *Database {
return &MockDatabase{posts: []*Post{}}
}),
godi.AddSingleton(func() *Logger {
return &MockLogger{}
}),
godi.AddScoped(NewRequestContext),
godi.AddScoped(NewPostService),
godi.AddScoped(NewPostController),
)
collection := godi.NewCollection()
collection.AddModules(testModule)
provider, _ := collection.Build()
defer provider.Close()
// Set up test router
router := gin.New()
router.Use(ScopeMiddleware(provider))
RegisterRoutes(router)
// Test creating a post
w := httptest.NewRecorder()
body := `{"title":"Test","content":"Content"}`
req := httptest.NewRequest("POST", "/posts", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, 201, w.Code)
var response Post
json.Unmarshal(w.Body.Bytes(), &response)
assert.Equal(t, "Test", response.Title)
}
Advanced Patterns
Authentication Middleware
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
return
}
// Validate token and get user ID
userID, err := validateToken(token)
if err != nil {
c.AbortWithStatusJSON(401, gin.H{"error": "Invalid token"})
return
}
// Add user ID to context for dependency injection
ctx := context.WithValue(c.Request.Context(), "userID", userID)
c.Request = c.Request.WithContext(ctx)
c.Set("userID", userID) // Also in Gin context for convenience
c.Next()
}
}
Error Handler Middleware
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
err := c.Errors.Last()
switch e := err.Err.(type) {
case *ValidationError:
c.JSON(400, gin.H{"error": e.Error()})
case *NotFoundError:
c.JSON(404, gin.H{"error": e.Error()})
default:
c.JSON(500, gin.H{"error": "Internal server error"})
}
}
}
}
Using Request Context in Services
// Any service can inject RequestContext
type AuditService struct {
db *Database
reqCtx *RequestContext
}
type AuditServiceParams struct {
godi.In
DB *Database
ReqCtx *RequestContext
}
func NewAuditService(params AuditServiceParams) *AuditService {
return &AuditService{
db: params.DB,
reqCtx: params.ReqCtx,
}
}
func (a *AuditService) LogAction(action string) {
log.Printf("[%s] User %s performed: %s",
a.reqCtx.RequestID,
a.reqCtx.UserID,
action)
}
Best Practices
1. Use Gin’s Built-in Features
// Use Gin's validation
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=2"`
Email string `json:"email" binding:"required,email"`
}
// Use Gin's binding
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(400, gin.H{"error": err.Error()})
return
}
3. Leverage Both Contexts
// Use Gin context for Gin-specific features
c.Set("requestID", requestID) // Store in Gin context
requestID := c.GetString("requestID") // Retrieve from Gin context
// Use request context for dependency injection
ctx := context.WithValue(c.Request.Context(), "userID", userID)
c.Request = c.Request.WithContext(ctx)
// Services can access the request context
func NewUserContext(ctx context.Context) *UserContext {
userID, _ := ctx.Value("userID").(string)
return &UserContext{UserID: userID}
}
Summary
Using Gin with godi provides:
Fast routing - Gin’s httprouter-based engine
Rich middleware - Easy to add cross-cutting concerns
Built-in validation - Struct tags for request validation
JSON handling - Automatic JSON binding and responses
Clean dependency injection - Controllers with godi
Request isolation - Scopes for each request
Easy testing - Mock dependencies and test with httptest
The key insight is that the scope’s context contains the scope itself, making it easy to pass through Gin’s request context!