# Shared Packages This directory contains shared Go packages used across all components in the monorepo. ## Package Overview | Package | Description | |---------|-------------| | `app` | Service bootstrapper with chi router, middleware, and graceful shutdown | | `config` | Viper-based configuration loading from environment variables | | `httpcontext` | Type-safe context key helpers for request-scoped data | | `httpclient` | Resilient HTTP client with automatic retries and exponential backoff | | `httpresponse` | Standard response envelope pattern for API responses | | `httpvalidation` | Struct validation wrapper around go-playground/validator | | `logging` | slog-based structured logging with context integration | | `middleware` | HTTP middleware: CORS, recovery, request ID, request logging | ## Quick Start ### Creating a New Service ```go package main import ( "net/http" "git.threesix.ai/jordan/testgo5/pkg/app" "git.threesix.ai/jordan/testgo5/pkg/httpresponse" ) func main() { // Create application with default middleware and health endpoints svc := app.New("my-service", app.WithDefaultPort(8080)) // Register routes svc.GET("/hello", func(w http.ResponseWriter, r *http.Request) { httpresponse.OK(w, r, map[string]string{"message": "Hello, World!"}) }) // Start server (blocks until shutdown signal) svc.Run() } ``` ## Package Documentation ### pkg/app Service bootstrapper that provides: - Chi router with standard middleware - Graceful shutdown handling - Health check endpoints (`/health`, `/ready`) ```go app := app.New("my-service", app.WithDefaultPort(8080), app.WithLogger(customLogger), ) // Register routes app.GET("/users/{id}", getUser) app.POST("/users", createUser) // Group routes app.Route("/api/v1", func(r chi.Router) { r.Get("/users", listUsers) }) // Register shutdown hooks app.OnShutdown(func(ctx context.Context) error { return db.Close() }) app.Run() ``` ### pkg/config Configuration loading from environment variables with Viper. ```go // Initialize configuration (once at startup) config.MustInit(config.Options{ AppName: "my-service", DefaultPort: 8080, }) // Read typed configuration appCfg := config.ReadAppConfig() // APP_NAME, APP_ENVIRONMENT, APP_DEBUG serverCfg := config.ReadServerConfig() // SERVER_HOST, SERVER_PORT, timeouts dbCfg := config.ReadDatabaseConfig() // DATABASE_URL, pool settings // Direct access dbURL := config.GetString("DATABASE_URL") debug := config.GetBool("APP_DEBUG") ``` **Environment Variables:** - `APP_NAME` - Application name (default: service name) - `APP_ENVIRONMENT` - development, staging, production - `APP_DEBUG` - Enable debug mode - `SERVER_HOST` - Server bind host (default: 0.0.0.0) - `SERVER_PORT` - Server port (default: 8080) - `DATABASE_URL` - Database connection string - `LOG_LEVEL` - debug, info, warn, error - `LOG_FORMAT` - json, text, auto ### pkg/httpcontext Type-safe context key helpers. ```go // Set values in middleware ctx := httpcontext.SetRequestID(r.Context(), requestID) ctx = httpcontext.SetUser(ctx, user) ctx = httpcontext.SetOrgID(ctx, orgID) // Get values in handlers requestID, ok := httpcontext.GetRequestID(ctx) user, ok := httpcontext.GetUser(ctx) orgID, ok := httpcontext.GetOrgID(ctx) // Panic if not found (use when middleware guarantees presence) user := httpcontext.MustGetUser(ctx) ``` ### pkg/httpclient HTTP client with automatic retries. ```go // Create client client := httpclient.New(httpclient.Config{ Timeout: 10 * time.Second, MaxRetries: 3, }) // Make requests resp, err := client.Do(req) // Convenience methods resp, err := httpclient.Get(ctx, "https://api.example.com/users") resp, err := httpclient.JSONPost(ctx, url, bytes.NewReader(jsonData)) ``` Retries on: - HTTP 5xx server errors - HTTP 429 Too Many Requests - Connection errors (timeout, refused) Does NOT retry on: - HTTP 4xx client errors (except 429) - Context cancellation ### pkg/httpresponse Standard response envelope for API responses. ```go // Success responses httpresponse.OK(w, r, data) // 200 OK httpresponse.Created(w, r, data) // 201 Created httpresponse.NoContent(w) // 204 No Content // Error responses httpresponse.BadRequest(w, r, "invalid input") httpresponse.Unauthorized(w, r, "authentication required") httpresponse.Forbidden(w, r, "insufficient permissions") httpresponse.NotFound(w, r, "user not found") httpresponse.InternalError(w, r, "something went wrong") // Validation errors with details httpresponse.ValidationError(w, r, "validation failed", details) // Decode request body var req CreateUserRequest if err := httpresponse.DecodeJSON(r, &req); err != nil { httpresponse.BadRequest(w, r, "invalid JSON") return } ``` **Response Format:** ```json { "data": { ... }, "error": { "code": "VALIDATION_ERROR", "message": "validation failed", "details": [ ... ] }, "meta": { "request_id": "abc-123", "timestamp": "2024-01-15T10:30:00Z" } } ``` ### pkg/httpvalidation Struct validation using go-playground/validator. ```go type CreateUserRequest struct { Email string `json:"email" validate:"required,email"` Name string `json:"name" validate:"required,min=2,max=100"` Phone string `json:"phone" validate:"omitempty,phone"` } // Validate struct if details := httpvalidation.ValidateStruct(req); len(details) > 0 { httpresponse.ValidationError(w, r, "validation failed", details) return } // Custom validators available: // - uuid: Valid UUID // - uuid_or_empty: Valid UUID or empty string // - phone: E.164 phone number format // - slug: URL-safe slug (lowercase, numbers, hyphens) // - hex_color: Hex color code (#RGB, #RRGGBB, #RRGGBBAA) ``` ### pkg/logging Structured logging with slog. ```go // Create logger logger := logging.New(logging.Config{ Level: logging.LevelInfo, Format: logging.FormatJSON, Environment: "production", }) // Or use convenience constructors logger := logging.NewDevelopment() // text format, debug level logger := logging.NewProduction() // JSON format, info level // Log messages logger.Info("user created", "user_id", userID) logger.Error("failed to connect", "error", err) // Create derived loggers reqLogger := logger.With("request_id", requestID) svcLogger := logger.WithService("user-service") // Get logger from context (set by middleware) logger := logging.FromContext(r.Context()) ``` ### pkg/middleware HTTP middleware for chi router. ```go r := chi.NewRouter() // Request ID generation/propagation r.Use(middleware.RequestID()) // Request logging r.Use(middleware.RequestLogger(logger)) // Panic recovery r.Use(middleware.Recoverer(logger)) // CORS r.Use(middleware.CORS(middleware.DefaultCORSConfig())) // Production CORS r.Use(middleware.CORS(middleware.CORSConfig{ AllowedOrigins: []string{"https://app.example.com"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowCredentials: true, })) ``` ## Guidelines - **Import Path**: Use `git.threesix.ai/jordan/testgo5/pkg/` for imports - **Keep packages focused**: Each package should do one thing well - **No circular dependencies**: pkg packages should not import from services/workers - **Document public APIs**: All exported functions should have doc comments - **Write tests**: Cover exported functions with unit tests