net/http Integration

Complete guide for using godi with Go’s standard net/http package.

Installation

go get github.com/junioryono/godi/v4
go get github.com/junioryono/godi/v4/http

Quick Start

package main

import (
    "encoding/json"
    "net/http"

    "github.com/junioryono/godi/v4"
    godihttp "github.com/junioryono/godi/v4/http"
)

type UserController struct{}

func NewUserController() *UserController {
    return &UserController{}
}

func (c *UserController) List(w http.ResponseWriter, r *http.Request) {
    json.NewEncoder(w).Encode([]string{"alice", "bob"})
}

func main() {
    services := godi.NewCollection()
    services.AddScoped(NewUserController)

    provider, _ := services.Build()
    defer provider.Close()

    mux := http.NewServeMux()
    mux.HandleFunc("GET /users", godihttp.Handle((*UserController).List))

    handler := godihttp.ScopeMiddleware(provider)(mux)
    http.ListenAndServe(":8080", handler)
}

ScopeMiddleware

Creates a request scope for each HTTP request.

mux := http.NewServeMux()
// ... register handlers ...

handler := godihttp.ScopeMiddleware(provider)(mux)
http.ListenAndServe(":8080", handler)

Configuration Options

handler := godihttp.ScopeMiddleware(provider,
    // Custom error handler for scope creation failures
    godihttp.WithErrorHandler(func(w http.ResponseWriter, r *http.Request, err error) {
        http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
    }),

    // Custom handler for scope close errors
    godihttp.WithCloseErrorHandler(func(err error) {
        log.Printf("Scope close error: %v", err)
    }),

    // Middleware that runs after scope creation
    godihttp.WithMiddleware(func(scope godi.Scope, r *http.Request) error {
        reqCtx := godi.MustResolve[*RequestContext](scope)
        reqCtx.UserID = r.Header.Get("X-User-ID")
        return nil
    }),
)(mux)

Handle

Wraps a controller method for type-safe resolution.

type UserController interface {
    List(http.ResponseWriter, *http.Request)
    GetByID(http.ResponseWriter, *http.Request)
    Create(http.ResponseWriter, *http.Request)
}

mux.HandleFunc("GET /users", godihttp.Handle(UserController.List))
mux.HandleFunc("GET /users/{id}", godihttp.Handle(UserController.GetByID))
mux.HandleFunc("POST /users", godihttp.Handle(UserController.Create))

Handler Options

mux.HandleFunc("GET /users", godihttp.Handle(UserController.List,
    // Enable panic recovery
    godihttp.WithPanicRecovery(true),

    // Custom panic handler
    godihttp.WithPanicHandler(func(w http.ResponseWriter, r *http.Request, v any) {
        log.Printf("Panic: %v", v)
        http.Error(w, "Unexpected error", http.StatusInternalServerError)
    }),

    // Custom scope error handler
    godihttp.WithScopeErrorHandler(func(w http.ResponseWriter, r *http.Request, err error) {
        http.Error(w, "Session error", http.StatusInternalServerError)
    }),

    // Custom resolution error handler
    godihttp.WithResolutionErrorHandler(func(w http.ResponseWriter, r *http.Request, err error) {
        http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
    }),
))

Complete Example

package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/google/uuid"
    "github.com/junioryono/godi/v4"
    godihttp "github.com/junioryono/godi/v4/http"
)

// === Services ===

type Logger struct{}

func NewLogger() *Logger { return &Logger{} }

func (l *Logger) Info(msg string, args ...any) {
    log.Printf("[INFO] "+msg, args...)
}

type RequestContext struct {
    ID        string
    UserID    string
    StartTime time.Time
}

func NewRequestContext() *RequestContext {
    return &RequestContext{
        ID:        uuid.New().String()[:8],
        StartTime: time.Now(),
    }
}

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

type UserService struct {
    reqCtx *RequestContext
    logger *Logger
}

func NewUserService(reqCtx *RequestContext, logger *Logger) *UserService {
    return &UserService{reqCtx: reqCtx, logger: logger}
}

func (s *UserService) GetAll() []User {
    s.logger.Info("[%s] Fetching all users", s.reqCtx.ID)
    return []User{{ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}}
}

// === Controllers ===

type UserController struct {
    service *UserService
    reqCtx  *RequestContext
}

func NewUserController(service *UserService, reqCtx *RequestContext) *UserController {
    return &UserController{service: service, reqCtx: reqCtx}
}

func (c *UserController) List(w http.ResponseWriter, r *http.Request) {
    users := c.service.GetAll()
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("X-Request-ID", c.reqCtx.ID)
    json.NewEncoder(w).Encode(map[string]any{
        "users":    users,
        "duration": time.Since(c.reqCtx.StartTime).String(),
    })
}

func (c *UserController) GetByID(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("X-Request-ID", c.reqCtx.ID)
    json.NewEncoder(w).Encode(User{ID: 1, Name: "User " + id})
}

// === Main ===

func main() {
    // Register services
    services := godi.NewCollection()
    services.AddSingleton(NewLogger)
    services.AddScoped(NewRequestContext)
    services.AddScoped(NewUserService)
    services.AddScoped(NewUserController)

    // Build provider
    provider, err := services.Build()
    if err != nil {
        log.Fatalf("Failed to build provider: %v", err)
    }
    defer provider.Close()

    // Get logger
    logger := godi.MustResolve[*Logger](provider)

    // Create mux
    mux := http.NewServeMux()

    // Routes
    mux.HandleFunc("GET /users", godihttp.Handle((*UserController).List))
    mux.HandleFunc("GET /users/{id}", godihttp.Handle((*UserController).GetByID))

    // Health check (no DI needed)
    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("OK"))
    })

    // Wrap with scope middleware
    handler := godihttp.ScopeMiddleware(provider,
        godihttp.WithMiddleware(func(scope godi.Scope, r *http.Request) error {
            reqCtx := godi.MustResolve[*RequestContext](scope)
            reqCtx.UserID = r.Header.Get("X-User-ID")
            return nil
        }),
    )(mux)

    // Create server
    server := &http.Server{
        Addr:         ":8080",
        Handler:      handler,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    // Graceful shutdown
    go func() {
        sigChan := make(chan os.Signal, 1)
        signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
        <-sigChan

        logger.Info("Shutting down server...")
        ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        defer cancel()
        server.Shutdown(ctx)
    }()

    // Start server
    logger.Info("Server starting on :8080")
    if err := server.ListenAndServe(); err != http.ErrServerClosed {
        log.Fatalf("Server error: %v", err)
    }
}

Wrap Helper

Alternative to Handle for creating http.Handler:

handler := godihttp.Wrap(func(ctrl *UserController, w http.ResponseWriter, r *http.Request) {
    ctrl.List(w, r)
})

mux.Handle("GET /users", handler)

Accessing URL Parameters (Go 1.22+)

Use r.PathValue() for path parameters:

func (c *UserController) GetByID(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // ...
}

Accessing Scope Manually

mux.HandleFunc("GET /custom", func(w http.ResponseWriter, r *http.Request) {
    scope, err := godi.FromContext(r.Context())
    if err != nil {
        http.Error(w, "No scope", http.StatusInternalServerError)
        return
    }

    service := godi.MustResolve[*UserService](scope)
    users := service.GetAll()
    json.NewEncoder(w).Encode(users)
})

Middleware Chaining

Compose with other middleware:

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
    })
}

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Compose middleware
handler := loggingMiddleware(
    godihttp.ScopeMiddleware(provider)(
        authMiddleware(mux),
    ),
)

Subrouting

Create subrouters with different middleware:

// Public routes
publicMux := http.NewServeMux()
publicMux.HandleFunc("GET /health", healthHandler)

// API routes with DI
apiMux := http.NewServeMux()
apiMux.HandleFunc("GET /users", godihttp.Handle((*UserController).List))
apiMux.HandleFunc("GET /users/{id}", godihttp.Handle((*UserController).GetByID))

// Compose
mainMux := http.NewServeMux()
mainMux.Handle("/", publicMux)
mainMux.Handle("/api/", http.StripPrefix("/api",
    godihttp.ScopeMiddleware(provider)(apiMux),
))

http.ListenAndServe(":8080", mainMux)

JSON Response Helper

Create a helper for consistent JSON responses:

func writeJSON(w http.ResponseWriter, status int, data any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func (c *UserController) List(w http.ResponseWriter, r *http.Request) {
    users := c.service.GetAll()
    writeJSON(w, http.StatusOK, map[string]any{
        "users": users,
    })
}

See also: Gin Integration | Chi Integration | Echo Integration | Fiber Integration