A Vukem application is a *vukem.App: configuration, logger, providers and long-running runners (the HTTP server, workers). The lifecycle is inspired by AdonisJS v7 and Goravel runners, written as plain Go with no globals and no reflection.
Overview#
Load config
.env files and the environment are merged, filled into vukem.Config and checked. Missing APP_KEY, debug in production or an invalid port abort boot.
Register
Every provider's Register runs in declared order. Register binds services and must not perform I/O.
Boot
Providers implementing Booter run Boot(ctx, app) in order. Boot may use services registered by other providers.
Run
Every runner starts in its own goroutine. Run blocks until the context is cancelled (for example by SIGTERM) or a runner fails.
Shutdown
Runners stop and are awaited, then OnShutdown hooks and providers' Shutdown run in reverse order, within SHUTDOWN_TIMEOUT.
Creating the app#
Applications build the app in one function, bootstrap.New, used by cmd/app and by the tests:
func New(opts ...vukem.Option) (*vukem.App, error) {
app, err := vukem.New(append([]vukem.Option{
vukem.WithProviders(Providers()...),
vukem.WithMigrations(migrations.All()),
vukem.WithViews(resources.Views()),
vukem.WithRoutes(routes.Web),
}, opts...)...)
if err != nil {
return nil, err
}
app.AddCommand(Commands()...)
return app, nil
}func main() { os.Exit(vukem.Main(bootstrap.New)) }vukem.Main(build) loads .env and the process environment, wires SIGINT/SIGTERM, and runs serve (the default) or the command named by the first argument. It exits 2 on usage errors and 1 on failures. Its testable body is vukem.RunMain(ctx, args, getenv, build, opts...).
| Option | Purpose |
|---|---|
WithEnv(fsys, getenv) | Load .env files from fsys and the environment from getenv (defaults: os.DirFS("."), os.LookupEnv) |
WithSource(src) | Use an already resolved *config.Source (tests: config.MapSource) |
WithLogger(l) / WithLogOutput(w) | Custom *slog.Logger, or where the default logger writes |
WithKind(k) | KindWeb (default), KindConsole or KindTest |
WithProviders(p...) | Providers, registered and booted in order |
WithRoutes(fn) | func(r *router.Router, app *vukem.App) error, called after providers registered; an error stops New |
WithViews(fsys) | Enable the view engine over fsys; views reload per request outside production |
WithMigrations(m) | Migrations used by migrate* commands and app.Migrator() |
WithDatabaseDrivers(d...) | Extra database drivers (postgres.Driver()) |
WithSeeders(s...) | Seeders run by db:seed and app.Seed |
WithListeners(r...) | Event listeners registered on app.Events |
WithMailTransport(t) | Replace the transport chosen by MAIL_MAILER |
WithClock(now) | Clock for sessions and other time-based features (vukemtest.FakeClock) |
WithRunner(name, r) | A long-running component started by Run |
WithoutHTTP() | Do not start the HTTP server runner (tests, workers) |
WithStdout(w) | Where commands print |
The App exposes Config, Source (every configuration value), Key (decoded APP_KEY), Log, Kind, Router, Views, Events and Telegram, plus DB(), Migrator(), Sessions(), Mailer(), Cache(), Seed(ctx), Handler() and Now().
Commands#
App commands run inside the app binary (vukem <command> delegates to it). Built in: migrate, migrate:rollback, migrate:reset, migrate:fresh, migrate:status, route:list, about. Add your own:
func SendReports() vukem.Command {
return vukem.CommandFunc{
CommandName: "reports:send",
Desc: "Email the weekly reports",
DefineFlags: func(fs *flag.FlagSet) { /* fs.Bool(...) */ },
Fn: func(ctx context.Context, app *vukem.App, args []string) error {
return nil
},
}
}Register it in bootstrap/commands.go (vukem make:command does it for you). Commands run with KindConsole, so providers limited to KindWeb are skipped.
Providers#
type Provider interface {
Register(app *App) error // bind services, no I/O
}
// Optional hooks, detected by interface assertion:
type Booter interface { Boot(ctx context.Context, app *App) error }
type Shutdowner interface { Shutdown(ctx context.Context, app *App) error }
type KindFilter interface { Kinds() []Kind }type DatabaseProvider struct {
DB *sql.DB
}
func (p *DatabaseProvider) Register(app *vukem.App) error {
db, err := sql.Open("sqlite", app.Source.Get("DATABASE_URL"))
if err != nil {
return fmt.Errorf("open database: %w", err)
}
p.DB = db
return nil
}
func (p *DatabaseProvider) Boot(ctx context.Context, app *vukem.App) error {
return p.DB.PingContext(ctx) // I/O belongs in Boot
}
func (p *DatabaseProvider) Shutdown(ctx context.Context, app *vukem.App) error {
return p.DB.Close()
}
// Kinds limits the provider to some runtimes; without it, it runs everywhere.
func (p *DatabaseProvider) Kinds() []vukem.Kind {
return []vukem.Kind{vukem.KindWeb, vukem.KindConsole}
}vukem.ProviderFunc adapts a plain function to a provider with only a Register phase.
Runners#
type Runner interface {
Run(ctx context.Context) error // block until ctx is done, then release resources
}vukem.RunnerFunc adapts a function. Runners can be added with WithRunner or, from a provider, with app.AddRunner(name, r). When one runner fails, the others are cancelled and the first error is returned from Run. A cancelled context is a clean exit (nil).
The HTTP server#
vukem.NewHTTPServer(addr, handler) returns a runner around *http.Server that is never configured with zero timeouts (Slowloris protection):
| Setting | Default |
|---|---|
ReadHeaderTimeout | 10s |
ReadTimeout | 30s |
WriteTimeout | 60s |
IdleTimeout | 120s |
MaxHeaderBytes | 1 MiB |
ShutdownTimeout | 15s drain of in-flight requests |
In-flight handlers keep the app's context values but are not cancelled when shutdown starts, so they can finish. Set Listener to serve on an existing listener and Ready to receive the bound address (useful in tests with port 0).
Shutdown#
app.OnShutdown(func(ctx context.Context) error {
return flushMetrics(ctx)
})Shutdown order: runners stop first, then OnShutdown hooks in reverse registration order, then providers' Shutdown in reverse provider order. All errors are joined. Shutdown is idempotent.
Request lifecycle#
net/http server
└─ server middleware every request, even unmatched (recover, request id, logger, static…)
└─ ServeMux match
├─ no match → 404 / 405 through the error handler
└─ router middleware matched routes only
└─ group / route middleware
└─ handler func(c *web.Ctx) error
└─ error? → error handler → HTML | redirect + flash | fragment | problem+jsonSee Middleware and Error handling.