Building Web Applications
This guide covers patterns for building production web applications with godi.
Application Structure
A typical godi web application:
myapp/
├── main.go # Application entry point
├── internal/
│ ├── config/ # Configuration loading
│ ├── database/ # Database connection
│ ├── middleware/ # HTTP middleware
│ ├── handlers/ # HTTP handlers
│ ├── services/ # Business logic
│ └── repositories/ # Data access
└── go.mod
Complete Example
Here’s a production-ready setup:
package main
import (
"context"
"database/sql"
"encoding/json"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/junioryono/godi/v4"
godihttp "github.com/junioryono/godi/v4/http"
_ "github.com/lib/pq"
)
// === Configuration ===
type Config struct {
DatabaseURL string
ServerAddr string
Debug bool
}
func NewConfig() *Config {
return &Config{
DatabaseURL: getEnv("DATABASE_URL", "postgres://localhost/myapp?sslmode=disable"),
ServerAddr: getEnv("SERVER_ADDR", ":8080"),
Debug: getEnv("DEBUG", "false") == "true",
}
}
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
// === Logger ===
type Logger struct {
*slog.Logger
}
func NewLogger(cfg *Config) *Logger {
level := slog.LevelInfo
if cfg.Debug {
level = slog.LevelDebug
}
return &Logger{
Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})),
}
}
// === Database ===
type Database struct {
*sql.DB
}
func NewDatabase(cfg *Config) (*Database, error) {
db, err := sql.Open("postgres", cfg.DatabaseURL)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
if err := db.Ping(); err != nil {
return nil, err
}
return &Database{DB: db}, nil
}
func (d *Database) Close() error {
return d.DB.Close()
}
// === Request Context ===
type RequestContext struct {
ID string
StartTime time.Time
UserID string
}
func NewRequestContext() *RequestContext {
return &RequestContext{
ID: generateID(),
StartTime: time.Now(),
}
}
func generateID() string {
return time.Now().Format("20060102150405.000000")
}
// === User Repository ===
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type UserRepository struct {
db *Database
log *Logger
}
func NewUserRepository(db *Database, log *Logger) *UserRepository {
return &UserRepository{db: db, log: log}
}
func (r *UserRepository) GetByID(ctx context.Context, id int) (*User, error) {
r.log.Debug("fetching user", "id", id)
var u User
err := r.db.QueryRowContext(ctx,
"SELECT id, name, email FROM users WHERE id = $1", id,
).Scan(&u.ID, &u.Name, &u.Email)
if err != nil {
return nil, err
}
return &u, nil
}
func (r *UserRepository) List(ctx context.Context) ([]User, error) {
rows, err := r.db.QueryContext(ctx, "SELECT id, name, email FROM users LIMIT 100")
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
return nil, err
}
users = append(users, u)
}
return users, rows.Err()
}
// === User Service ===
type UserService struct {
repo *UserRepository
reqCtx *RequestContext
log *Logger
}
func NewUserService(repo *UserRepository, reqCtx *RequestContext, log *Logger) *UserService {
return &UserService{repo: repo, reqCtx: reqCtx, log: log}
}
func (s *UserService) GetUser(ctx context.Context, id int) (*User, error) {
s.log.Info("getting user",
"request_id", s.reqCtx.ID,
"user_id", id,
)
return s.repo.GetByID(ctx, id)
}
func (s *UserService) ListUsers(ctx context.Context) ([]User, error) {
s.log.Info("listing users", "request_id", s.reqCtx.ID)
return s.repo.List(ctx)
}
// === User Controller ===
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, err := c.service.ListUsers(r.Context())
if err != nil {
http.Error(w, "Failed to fetch users", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Request-ID", c.reqCtx.ID)
json.NewEncoder(w).Encode(users)
}
// === Main ===
func main() {
// Register services
services := godi.NewCollection()
// Singletons - shared infrastructure
services.AddSingleton(NewConfig)
services.AddSingleton(NewLogger)
services.AddSingleton(NewDatabase)
// Scoped - per-request
services.AddScoped(NewRequestContext)
services.AddScoped(NewUserRepository)
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 config and logger
cfg := godi.MustResolve[*Config](provider)
logger := godi.MustResolve[*Logger](provider)
// Setup routes
mux := http.NewServeMux()
mux.HandleFunc("GET /users", godihttp.Handle((*UserController).List))
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
// Wrap with middleware
handler := godihttp.ScopeMiddleware(provider,
godihttp.WithMiddleware(func(scope godi.Scope, r *http.Request) error {
// Add request ID to context for logging
reqCtx := godi.MustResolve[*RequestContext](scope)
logger.Debug("request started",
"request_id", reqCtx.ID,
"method", r.Method,
"path", r.URL.Path,
)
return nil
}),
)(mux)
// Create server
server := &http.Server{
Addr: cfg.ServerAddr,
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", "addr", cfg.ServerAddr)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}
Key Patterns
1. Layered Architecture
┌─────────────────────────────────────────────┐
│ Controllers (HTTP handlers) │
│ - Parse requests │
│ - Call services │
│ - Return responses │
├─────────────────────────────────────────────┤
│ Services (Business logic) │
│ - Orchestrate operations │
│ - Apply business rules │
│ - Coordinate repositories │
├─────────────────────────────────────────────┤
│ Repositories (Data access) │
│ - Database queries │
│ - Cache operations │
│ - External API calls │
├─────────────────────────────────────────────┤
│ Infrastructure (Shared resources) │
│ - Database connections │
│ - Logger │
│ - Configuration │
└─────────────────────────────────────────────┘
2. Lifetime Assignments
// Singleton: Infrastructure
services.AddSingleton(NewConfig)
services.AddSingleton(NewLogger)
services.AddSingleton(NewDatabase)
services.AddSingleton(NewHTTPClient)
// Scoped: Per-request state and services
services.AddScoped(NewRequestContext)
services.AddScoped(NewUserRepository)
services.AddScoped(NewUserService)
services.AddScoped(NewUserController)
// Transient: Utilities (rarely needed)
services.AddTransient(NewQueryBuilder)
3. Request Context Pattern
Share request-specific data across services:
type RequestContext struct {
ID string
UserID string
StartTime time.Time
Logger *slog.Logger
}
func NewRequestContext(logger *Logger) *RequestContext {
id := uuid.New().String()
return &RequestContext{
ID: id,
StartTime: time.Now(),
Logger: logger.With("request_id", id),
}
}
4. Middleware Integration
Set request data before handlers run:
handler := godihttp.ScopeMiddleware(provider,
// Logging middleware
godihttp.WithMiddleware(func(scope godi.Scope, r *http.Request) error {
reqCtx := godi.MustResolve[*RequestContext](scope)
reqCtx.Logger.Info("request started",
"method", r.Method,
"path", r.URL.Path,
)
return nil
}),
// Auth middleware
godihttp.WithMiddleware(func(scope godi.Scope, r *http.Request) error {
reqCtx := godi.MustResolve[*RequestContext](scope)
if userID := r.Header.Get("X-User-ID"); userID != "" {
reqCtx.UserID = userID
}
return nil
}),
)(mux)
5. Database Transaction Per Request
type Transaction struct {
tx *sql.Tx
}
func NewTransaction(db *Database) (*Transaction, error) {
tx, err := db.Begin()
if err != nil {
return nil, err
}
return &Transaction{tx: tx}, nil
}
func (t *Transaction) Close() error {
return t.tx.Commit()
}
// All repositories use the same transaction
type UserRepository struct {
tx *Transaction
}
type OrderRepository struct {
tx *Transaction // Same transaction!
}
Graceful Shutdown
Clean shutdown in the right order:
// 1. Stop accepting new requests
server.Shutdown(ctx)
// 2. Wait for in-flight requests to complete
// (handled by Shutdown)
// 3. Close provider (closes database, etc.)
provider.Close()
Next: Learn about testing with godi