// Package api provides HTTP service infrastructure for rdev. package api import ( "context" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) // Router is an alias for chi.Router, exposing it for handler mounting. type Router = chi.Router // App is the main application struct that provides HTTP infrastructure. // It manages routing, logging, and graceful shutdown. type App struct { name string port int logger *slog.Logger router chi.Router server *http.Server onShutdown []func(context.Context) error } // Option configures the App. type Option func(*App) // WithPort sets the server port. func WithPort(port int) Option { return func(a *App) { a.port = port } } // WithLogger sets a custom logger. func WithLogger(logger *slog.Logger) Option { return func(a *App) { a.logger = logger } } // New creates a new App instance. // Note: Call Use() to add middleware before mounting routes. func New(name string, opts ...Option) *App { app := &App{ name: name, port: 8080, onShutdown: make([]func(context.Context) error, 0), } for _, opt := range opts { opt(app) } if app.logger == nil { app.logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, })) } app.router = chi.NewRouter() app.setupMiddleware() // Health endpoints are set up in Run() to allow middleware to be added first return app } // setupMiddleware configures the standard middleware stack. func (a *App) setupMiddleware() { a.router.Use(middleware.RequestID) a.router.Use(middleware.RealIP) a.router.Use(middleware.Logger) a.router.Use(middleware.Recoverer) a.router.Use(middleware.Timeout(60 * time.Second)) } // setupHealthEndpoints registers /health and /ready endpoints. func (a *App) setupHealthEndpoints() { a.router.Get("/health", func(w http.ResponseWriter, r *http.Request) { WriteSuccess(w, r, map[string]string{ "status": "ok", "service": a.name, }) }) a.router.Get("/ready", func(w http.ResponseWriter, r *http.Request) { WriteSuccess(w, r, map[string]string{ "status": "ready", "service": a.name, }) }) } // Logger returns the application logger. func (a *App) Logger() *slog.Logger { return a.logger } // Router returns the underlying chi router. func (a *App) Router() chi.Router { return a.router } // Use appends middleware to the router middleware stack. func (a *App) Use(middlewares ...func(http.Handler) http.Handler) { a.router.Use(middlewares...) } // GET registers a handler for GET requests. func (a *App) GET(pattern string, handler http.HandlerFunc) { a.router.Get(pattern, handler) } // POST registers a handler for POST requests. func (a *App) POST(pattern string, handler http.HandlerFunc) { a.router.Post(pattern, handler) } // PUT registers a handler for PUT requests. func (a *App) PUT(pattern string, handler http.HandlerFunc) { a.router.Put(pattern, handler) } // PATCH registers a handler for PATCH requests. func (a *App) PATCH(pattern string, handler http.HandlerFunc) { a.router.Patch(pattern, handler) } // DELETE registers a handler for DELETE requests. func (a *App) DELETE(pattern string, handler http.HandlerFunc) { a.router.Delete(pattern, handler) } // Route creates a new sub-router with the given pattern prefix. func (a *App) Route(pattern string, fn func(r chi.Router)) { a.router.Route(pattern, fn) } // Mount attaches a sub-router or http.Handler at the given pattern. func (a *App) Mount(pattern string, handler http.Handler) { a.router.Mount(pattern, handler) } // OnShutdown registers a function to be called during graceful shutdown. func (a *App) OnShutdown(fn func(context.Context) error) { a.onShutdown = append(a.onShutdown, fn) } // Run starts the HTTP server and blocks until shutdown. func (a *App) Run() { // Set up health endpoints after all middleware has been added a.setupHealthEndpoints() addr := fmt.Sprintf(":%d", a.port) a.server = &http.Server{ Addr: addr, Handler: a.router, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 120 * time.Second, } errChan := make(chan error, 1) go func() { a.logger.Info("starting server", "service", a.name, "address", addr, ) if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { errChan <- err } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) select { case err := <-errChan: a.logger.Error("server error", "error", err) os.Exit(1) case sig := <-quit: a.logger.Info("received shutdown signal", "signal", sig.String()) } a.shutdown() } // shutdown performs graceful shutdown of the application. func (a *App) shutdown() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() a.logger.Info("shutting down server") if err := a.server.Shutdown(ctx); err != nil { a.logger.Error("server shutdown error", "error", err) } for _, fn := range a.onShutdown { if err := fn(ctx); err != nil { a.logger.Error("shutdown hook error", "error", err) } } a.logger.Info("server stopped", "service", a.name) } // ServeHTTP implements http.Handler, allowing App to be used in tests. func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { a.router.ServeHTTP(w, r) } // ListenAddr returns the address the server is configured to listen on. func (a *App) ListenAddr() string { return fmt.Sprintf(":%d", a.port) }