Anvil API
The Anvil package is github.com/TDB-Group/anvil.
Application main packages use it to build the app, provide dependencies,
install drivers and plugins, run generated wiring, and start the process.
Read Callbacks and Middleware for the exact difference between startup hooks, request middleware, error observers, and event subscribers.
App Construction
Section titled “App Construction”app := anvil.New( httpstd.Driver(), anvilgrpc.Driver(), anvil.WithProviders( anvil.As[project.Store](project.NewMemoryStore()), ), anvil.Use(telemetry.New()), anvil.OnError(captureError),)New returns an *anvil.App. Options never start listeners. Startup happens
when the app is wired and then run.
New records option errors and returns them later from Wire or Run.
Nil options are recorded as initialization errors. That keeps construction
panic-free while still failing before the app starts.
Sentinel Errors
Section titled “Sentinel Errors”Anvil exposes two sentinel errors for callers that need errors.Is:
var ErrAlreadyWired errorvar ErrTransportNotRegistered errorErrAlreadyWired is returned when app.Wire(...) is called more than once.
ErrTransportNotRegistered wraps missing transport errors from WireContext.
Generated wiring uses that path when it needs a protocol driver that was not
registered in anvil.New(...).
Driver Registration
Section titled “Driver Registration”Driver packages return anvil.Option values. A driver option calls
anvil.WithTransport internally.
app := anvil.New( httpstd.Driver(), anvilgraphql.Driver(), anvilgrpc.Driver(),)Duplicate protocol names are rejected. A generated HTTP module requires a
registered sdk.HTTPTransport; generated gRPC, GraphQL, and queue modules
require their matching SDK transport interfaces.
Generated wiring also looks up transports by the standard protocol string:
| Generated wiring | Required Protocol() | Required SDK contract |
|---|---|---|
| HTTP routes | http | sdk.HTTPTransport |
| WebSocket routes | First registered sdk.WebSocketTransport | sdk.WebSocketTransport |
| GraphQL endpoints | graphql | sdk.GraphQLTransport |
| gRPC services | grpc | sdk.GRPCTransport |
| Queue jobs | queue | sdk.QueueTransport |
Custom drivers for generated Anvil apps use those protocol strings. A transport
with another protocol name can still be registered for custom runtime work, but
generated Wire code will not find it through WireContext.HTTP,
WireContext.GraphQL, WireContext.GRPC, or WireContext.Queue.
Transports must be registered before the app starts. Once Run, Listen,
RunTLS, or ListenTLS finalizes transports, later registration returns an
error.
Low-level integrations can register a transport directly:
app := anvil.New( anvil.WithTransport(customTransport),)Transports that also implement http.Handler become edge targets when they
implement sdk.HTTPTransport, sdk.GraphQLTransport, or sdk.GRPCTransport.
Anvil then serves them behind the shared HTTP-family listener. Other transports
run as background transports and receive an empty address when Start is
called.
Anvil checks edge target contracts in this order when a custom transport implements more than one: GraphQL, then gRPC, then HTTP. Official drivers keep one edge protocol per driver.
RegisterTransport(transport) is the method form for custom composition code.
Transport protocols must be non-empty and unique. After Run, Listen,
RunTLS, or ListenTLS begins finalizing transports, later transport
registration returns an error.
The normal multi-protocol runtime is HTTP-family edge based. Anvil does
not ask HTTP, GraphQL, or gRPC drivers to bind their own public port when those
drivers also implement http.Handler; it registers them behind the shared edge
listener and calls the selected driver as a handler.
Providers
Section titled “Providers”Providers register application-owned dependencies before generated wiring runs.
app := anvil.New( anvil.WithProviders( anvil.As[project.Store](project.NewMemoryStore()), anvil.Named[project.Store]("read", project.NewReadStore()), anvil.Named[project.Store]("write", project.NewWriteStore()), ),)As[T](value) registers a singleton value for the named Go type T. Use it
when the injection point is an interface:
type Projects struct { Store project.Store `inject:""`}Named[T](name, value) registers the same Go type with an explicit name. Use
it when two fields intentionally use the same type:
type Projects struct { ReadStore project.Store `inject:"read"` WriteStore project.Store `inject:"write"`}Factory[T] and NamedFactory[T] build dependencies while Anvil wires the app:
anvil.Factory(func(r sdk.DependencyResolver) (*Service, error) { store, err := anvil.Resolve[project.Store](r) if err != nil { return nil, err } return NewService(store), nil})Provider type keys require named Go types. Built-in types, anonymous structs, and unnamed slices or maps are rejected as provider keys. A named type whose underlying type is a slice, map, or struct is accepted because it has a stable package path and type name.
Pointer types and value types share the same provider key because the key is
based on the named element type. The produced value still has to match the
field type Anvil resolves. A field of type *Config needs a provider that
returns *Config; a field of type Config needs a provider that returns
Config.
RegisterProvider(provider) is the method form used by plugins and custom
composition code. It follows the same timing rule as WithProviders: call it
before Wire. Registering providers after wiring returns an error.
Provider keys need to be non-empty when generated code will resolve them.
Providers created by As, Named, Factory, and NamedFactory always have
keys. If a custom provider returns an empty key, Anvil calls Build
immediately and does not register it in the generated DI container. Use a real
key for dependencies that generated code resolves.
Provider registration returns an error immediately when the provider is nil,
the app is nil, the provider type is not a named Go type, or the generated DI
container already has the same key. Factory errors are returned when generated
wiring resolves that provider. If a provider factory is nil, returns an error,
or returns a value with the wrong Go type, Wire fails when the generated graph
reaches that dependency.
MustResolve panics by design. Use it only inside startup-only provider
factories when the app needs a hard startup failure.
Wiring
Section titled “Wiring”Generated packages register a wiring function from init.
import _ "example.com/portal/anvilgen"
if err := app.Wire(); err != nil { log.Fatal(err)}With no arguments, Wire uses every registered generated wiring function.
If no generated package was imported and no explicit wiring function is passed,
Wire has nothing to register. It still marks the app as wired, so a later
call returns ErrAlreadyWired. An app wired this way starts with no generated
routes, services, or jobs.
Tests can call explicit wiring instead:
if err := app.Wire(anvilgen.Wiring()); err != nil { t.Fatal(err)}Wire can run once. A second call returns ErrAlreadyWired.
Generated packages call:
anvil.RegisterWiring(anvilgen.Wiring())RegisterWiring(fn) adds a generated wiring function to the package-level
registry. It returns a cleanup function for tests that need to unregister a
temporary wiring function. Passing nil registers nothing and returns a no-op
cleanup function.
Generated wiring uses WireContext to access the DI injector and registered
transports. Application code normally does not use WireContext directly.
Run does not call Wire. Generated applications call Wire first so routes,
providers, middleware, queue handlers, and protocol metadata are registered
before the listener starts.
Listener and Run Methods
Section titled “Listener and Run Methods”Run starts with an explicit context:
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)defer cancel()
if err := app.Run(ctx, ":8080"); err != nil { log.Fatal(err)}Listen creates that interrupt context for you. It handles os.Interrupt.
Use Run with your own context when the process needs custom cancellation,
such as SIGTERM handling in containers.
if err := app.Listen(":8080"); err != nil { log.Fatal(err)}TLS variants set cert and key before running:
app.ListenTLS(":443", "cert.pem", "key.pem")app.RunTLS(ctx, ":443", "cert.pem", "key.pem")Cert and key must both be non-empty.
Treat an *anvil.App as one process lifecycle. Build it, register drivers and
providers, wire generated components, run it, then let it shut down. Create a
new app value for another run after Run or Listen returns. Wire is
one-shot, transport finalization is one-shot, and several drivers keep
shutdown state that is not designed to restart.
Run follows this order:
- Return any initialization error recorded by options.
- Finalize transports into edge targets and background transports.
- Register the edge transport with the core runtime when at least one HTTP-family target exists.
- Run boot hooks in registration order.
- Start registered core transports concurrently.
- Wait for context cancellation or the first transport return.
- Shut down transports.
- Run shutdown hooks in reverse registration order.
Run requires a non-nil context and a non-empty address. If finalization leaves
the core runtime with no transports, startup fails with core: no transports registered. That usually means no driver was installed, or the app registered
only custom transports that were rejected earlier as initialization errors.
Canceling the run context is a clean stop signal. When the context is canceled,
Anvil shuts down transports and returns the shutdown error, if any. A clean
context cancellation with clean transport shutdown returns nil rather than
context.Canceled.
If the context is canceled, shutdown runs with context.WithoutCancel(ctx) so
cleanup hooks can still use the original context values. If a transport returns
first, Anvil still shuts down the rest and returns the joined transport and
shutdown errors.
If a boot hook returns an error, transports have not started yet. Startup fails
with that hook error and Run does not run shutdown hooks for that failed boot.
Transport Start(addr) is a blocking call in the Anvil runtime model. A driver
that returns from Start tells the core runtime that the transport has stopped
or failed. Returning nil still causes the app to begin shutdown, because one
registered transport has ended.
For HTTP-family drivers behind the shared edge, the driver Start(addr) method
is not called in normal app runtime. The core runtime calls Start(addr) on the
edge transport. The edge serves the public socket and calls HTTP, GraphQL, or
gRPC drivers as http.Handler targets after request classification. The target
drivers still receive Shutdown(ctx) when the edge stops.
Custom Listener
Section titled “Custom Listener”WithListener configures the edge net/http.Server:
app := anvil.New( anvil.WithListener(&http.Server{ ReadHeaderTimeout: 2 * time.Second, MaxHeaderBytes: 32 << 10, }), httpstd.Driver(),)Handler must be nil because Anvil installs the handler that classifies HTTP,
GraphQL, and gRPC requests before drivers run.
Passing nil, or passing a server with Handler already set, records an
initialization error. New still returns an app, and Wire or Run reports
the error before the process starts accepting traffic.
Run and Listen set the server address from the addr argument. The
http.Server.Addr value on the supplied listener config is ignored.
WithListener is ignored unless at least one HTTP-family driver is registered.
Queue-only apps run their queue transports directly through the core runtime.
An HTTP-family driver counts as an edge driver only when it implements both
http.Handler and its SDK transport contract. A transport that lacks
http.Handler still stays registered for generated wiring, but at runtime it
is started as a background transport with an empty address.
Anvil copies the supplied http.Server struct when it builds the edge listener.
Pointer fields such as TLSConfig and Protocols are still shared values, so
configure them before app startup and treat them as immutable after passing the
server to WithListener.
Trusted Proxy Client IP
Section titled “Trusted Proxy Client IP”WithProxy configures client-IP resolution for applications behind reverse
proxies:
app := anvil.New( anvil.WithProxy(anvil.ProxyConfig{ ProxyHeader: "CF-Connecting-IP", TrustedProxies: []string{"203.0.113.0/24"}, }), httpstd.Driver(),)type ProxyConfig struct { ProxyHeader string TrustedProxies []string}ProxyHeader is the header written by the trusted proxy, such as
CF-Connecting-IP, X-Forwarded-For, or Forwarded.
TrustedProxies contains CIDR ranges or literal IP addresses for direct peers
that are allowed to set that header. If the direct peer is not trusted, Anvil
ignores the header and uses the socket peer IP.
Handlers and middleware read the result with ctx.Request().IP().
Plugins
Section titled “Plugins”anvil.Use registers plugins during construction:
app := anvil.New( anvil.Use(telemetry.New()),)app.Use(plugin) is the method form. Plugin names must be non-empty and unique.
If Register returns an error, the plugin name reservation is released and app
startup fails with that error.
Plugins can register boot hooks, shutdown hooks, providers, error observers,
error mappers, and event subscribers through sdk.AppLifecycle.
anvil.Use(...) registers plugins while New applies options. app.Use(...)
registers the plugin at the call site. In both forms, boot hooks run later,
after wiring and before transports start.
Lifecycle Methods
Section titled “Lifecycle Methods”The method forms are useful when setup happens after New:
app := anvil.New(httpstd.Driver())
app.OnBoot(checkDependencies)app.OnShutdown(flushTelemetry)app.OnError(captureError)OnBoot hooks run before transports start. If a boot hook returns an error,
startup fails.
OnShutdown hooks run during graceful shutdown.
OnError observes mapped failures from drivers, generated code, panics, and
the error pipeline. Observers run synchronously in registration order, so slow
observers need their own buffering or timeouts.
EventBus() returns the shared in-process event bus:
app.EventBus().Subscribe("project.created", func(payload any) { // Handle cross-cutting fanout.})The event bus publishes synchronously on the publishing goroutine. It is useful for deterministic plugin fanout and tests; it is not a background job runner.
Event handlers run in subscription order for a topic. Anvil does not recover panics from event handlers and does not add timeouts around them. Handlers that call external systems or start background work need their own buffering, timeouts, and panic recovery.
Errors
Section titled “Errors”anvil.OnError registers an error observer during construction:
app := anvil.New( anvil.OnError(func(ctx context.Context, event sdk.ErrorEvent) { logger.Error("request failed", "status", event.Failure.Status) }),)app.ErrorPipeline() exposes the mapper surface:
app.ErrorPipeline().Use(DomainMapper{})It returns the SDK ErrorPipeline interface, not a concrete core type.
Config Helpers
Section titled “Config Helpers”Env returns an environment value or fallback:
addr := anvil.Env("ADDR", ":8080")The fallback is returned when the key is empty, the variable is missing, or the variable is set to an empty string.
LoadConfig[T] fills a struct from env, default, and required:"true"
tags at boot:
type Config struct { Addr string `env:"ADDR" default:":8080"` Mode anvil.Mode `env:"APP_ENV" default:"development"` TTL time.Duration `env:"TOKEN_TTL" default:"15m"`}
cfg, err := anvil.LoadConfig[Config]()Defaults are used only when the environment variable is missing. If the
variable is set to an empty string, the default is not applied. A field tagged
required:"true" fails when the final value is empty.
LoadConfig[T] requires T to be a struct. It walks settable fields only.
Unexported fields are ignored because Go reflection cannot assign to them.
Fields without an env tag are ignored unless they are nested structs.
Supported field types:
stringbool, parsed withstrconv.ParseBool- Signed and unsigned integers, parsed as base-10 numbers
- Floats
time.Duration, parsed withtime.ParseDuration[]stringfrom comma-separated values, with each item trimmed- Nested structs, except
time.Duration
For []string, Anvil trims whitespace around each comma-separated item but
does not remove empty items. FEATURES=a,,b becomes []string{"a", "", "b"}.
MustConfig[T] panics by design and is only for startup code.
Mode Helpers
Section titled “Mode Helpers”ParseMode accepts:
| Input | Mode |
|---|---|
"", dev, development, local | anvil.Development |
prod, production | anvil.Production |
test, testing | anvil.Test |
ModeFromEnv() checks ANVIL_ENV, APP_ENV, and GO_ENV by default and
returns development when none are set.
ModeFromEnv("APP_MODE", "GO_ENV") checks only the supplied keys, in order.
Passing an empty key returns an error.
MustMode panics by design and is only for startup code.
Mode also has predicate methods for readable branch points:
mode.IsDevelopment()mode.IsProduction()mode.IsTest()