Introduction
Gas is a modular Go framework for building SaaS backends. It provides the plumbing every product needs (dependency injection, routing, middleware, events, migrations, and service lifecycle), so that each new product starts from working wiring instead of rebuilding it.
It has two layers. The core package, github.com/gasmod/gas, is the framework
itself. Around it sit optional modules for configuration, logging, databases,
migrations, caching, authentication, templates and HTML, object storage,
email, and job queues. Each module is its own Go module, so go get pulls in
only what you use.
The problem
Section titled “The problem”A typical Go service starts with net/http or a small router and grows its
own wiring: a main that constructs everything in the right order, passes
dependencies by hand, starts background goroutines, and hopefully closes things
in the right order on shutdown. Every new project repeats it, slightly
differently, and the mistakes in it (a dependency constructed twice, a
connection never closed, a handler that needs something nobody registered) tend
to show up at runtime.
Gas replaces that wiring with a container that knows how to build, start, and stop your application, and that checks the wiring before the first request.
What it looks like
Section titled “What it looks like”import ( "context" "log" "net/http"
"github.com/gasmod/gas" gaslog "github.com/gasmod/gas/log")
// Greeter is a service. The container builds it, calls Init at startup, and// calls Close at shutdown.type Greeter struct { router *gas.Router logger gas.Logger}
// NewGreeter declares its dependencies as parameters; the container supplies// them.func NewGreeter(router *gas.Router, logger gas.Logger) *Greeter { return &Greeter{router: router, logger: logger}}
func (g *Greeter) Name() string { return "greeter" }
func (g *Greeter) Init(context.Context) error { g.router.Get(g, "/hello/{name}", func(ctx gas.Context) error { g.logger.Info("greeting").Str("name", ctx.Param("name")).Send() return ctx.Text(http.StatusOK, "Hello, "+ctx.Param("name")) }) return nil}
func (g *Greeter) Close(context.Context) error { return nil }
func main() { app := gas.NewApp( gas.WithSingletonService[gas.Logger](gaslog.NewSlogLogger()), gas.WithSingletonService[*Greeter](NewGreeter), )
if err := app.Run(); err != nil { log.Fatal(err) }}Greeter is a service: a constructor that declares its dependencies as
parameters, and three methods. The container builds it after the router and
logger it asks for, calls Init at startup, where it registers its route, and
calls Close at shutdown. app.Run() starts everything, serves HTTP on
0.0.0.0:8080 by default, and shuts down on SIGINT or SIGTERM.
The design
Section titled “The design”Everything is a service. Features, and the infrastructure under them, are
packaged as types with a constructor, Init, and Close. The container
initializes them in dependency order and closes them in reverse.
See Services and DI.
Dependencies are injected by interface. Core declares provider interfaces
such as gas.Logger, gas.ConfigProvider, gas.DatabaseProvider, and
gas.CacheProvider. Modules implement them and are registered under them, and
your services ask for the interfaces, never for a module’s concrete type. The
container resolves by exact type, with no guessing about which registration
satisfies an interface. Swapping a backend, or a test mock, is a change to one
registration.
Modules are separate and independent. No module’s production code imports
another module; they meet only through the interfaces in core. A service that
depends on gas.CacheProvider works the same against the in-memory cache, the
Valkey cache, or a mock. See Modules.
Wiring mistakes fail at startup. A missing registration, a service that
implements only part of the lifecycle, a singleton holding a per-request value,
a handler parameter nobody registered: each one fails Start with an error
naming the types involved, before the server accepts a request.
Services own what they register. Every route, named middleware, and event subscription records the service that registered it. That makes the route map attributable, and it makes it possible to close one service at runtime, with its routes answering 503, without restarting the process. See Ownership and teardown.
Services do not import each other. They talk through the event bus and through interfaces, so features can be added and removed independently.
HTTP is optional. gas.App serves HTTP; gas.Worker is the same container
and lifecycle without a server, for queue consumers, Lambda functions, and CLI
tools. See Workers.
When Gas fits
Section titled “When Gas fits”Gas is built for small teams and solo developers shipping SaaS backends: JSON APIs, server-rendered HTML apps with HTMX, background workers, and AWS-hosted services. It fits when you want conventions for structure and lifecycle, and want the infrastructure (PostgreSQL or SQLite, Valkey, S3, SES, SQS) behind interfaces you can swap and mock.
It is a poorer fit when:
- You want a minimal router and nothing else. Gas’s container and lifecycle are
the point; if you do not want them,
net/httpor chi alone is simpler. - You need a stable, 1.0 API. Gas is pre-1.0, and minor versions can contain breaking changes.
- Your infrastructure is outside what the modules cover and you do not want to write the provider yourself. Any provider interface can be implemented by your own type, but the official modules target the backends listed above.