Project structure
Gas does not require any layout: an app is a gas.App with services
registered on it, and the code can live wherever Go allows. This page is a
recommendation. It is the layout the example apps in
example/ use, and the one
the tutorial builds.
The layout
Section titled “The layout”Directorymyapp/
- go.mod
- config.json settings for local runs, or a .env file
Directorycmd/
- main.go loads, builds, and runs the app
Directoryapp/
- app.go New wires modules, services, and middleware
- errors.go app-wide pieces such as a custom error handler
- app_test.go starts the whole app and tests it over HTTP
Directorynotes/ one package per service
- service.go
- handlers.go
Directorymigrations/
- 20261021001_create_notes.up.sql
- 20261021001_create_notes.down.sql
Directorybilling/
- …
Directorydb/ generated query code, if you use sqlc
- …
cmd/: the entry point
Section titled “cmd/: the entry point”main does as little as possible: load the config, build the app, run it,
and exit on error. Everything worth testing lives in packages it imports.
// Command notesapi serves the notes API built in the tutorial.package main
import ( "log"
"example.com/notesapi/app")
func main() { cfg, err := app.LoadConfig("config.json") if err != nil { log.Fatal(err) }
if err := app.New(cfg).Run(); err != nil { log.Fatal(err) }}In example/api-server, main is shorter still, because its app.New
loads the config itself. example/lambda-worker/cmd/main.go builds a
gas.Worker instead of an app and hands its handler to the Lambda runtime;
see Workers.
app/: the wiring
Section titled “app/: the wiring”app.New returns a *gas.App with everything registered and does not run it.
It is the one place that says what the app is made of:
- the config, registered under
gas.ConfigProvider; - the logger, under
gas.Logger; - the infrastructure modules, each under the interface services ask for, such
as
gas.DatabaseProviderandgas.MigrationManager; - the app’s own services, under their concrete types;
- the error handler and global middleware.
Returning the app unstarted is what makes the whole app testable. A test calls
New, can register replacements for external dependencies, then calls
Start and serves Handler() from httptest. The tutorial’s test runs the
app as is; example/api-server/app/app_test.go swaps its database, storage,
queue, email, and cache for in-process doubles first. See
Testing.
App-wide types that are not services also go here:
example/api-server/app/errors.go holds its custom error handler, and
request_logger.go its per-request logger type.
One package per service
Section titled “One package per service”Each feature is a service in its own package, named
after what it does (notes, files, shares). The package holds:
service.go: theServicetype, its constructor, andName,Init, andClose.Initregisters the service’s routes, middleware, subscriptions, and migrations, so everything the service owns is declared in one place.- the handlers, in the same file or beside it.
migrations/: the service’s.up.sqland.down.sqlfiles, embedded with//go:embed migrations/*.sqland registered inInitwithRegisterFS. The service that owns a table owns its schema. See Migrations.- any other files the service embeds. In
example/api-server, thesharesservice embeds its notification template fromshares/templates/.
A service depends on other services and modules through its constructor parameters, never through package-level state. A package per service keeps those dependencies visible in its imports, and keeps one service from reaching into another’s internals.
example/api-server has three such packages: auth, files, and shares.
A small app can start with its services as files in app/, as
example/hello-world-2 does with greet_module.go and notes_module.go,
and move each one out when it grows a table or a second file.
Configuration files
Section titled “Configuration files”The config file sits in the project root, and the app is run from there: reading from disk, the JSON and .env providers only open files inside the working directory.
example/templates-basicandexample/hello-world-2readconfig.json.example/api-serverreads a.envfile when there is one, and environment variables on top. It commits.env.exampleinstead of.env.
The tutorial reads config.json and then the environment, so the file holds
local defaults and the environment overrides them in production. See
Config for the providers and how keys map to
environment variables.
Generated code and assets
Section titled “Generated code and assets”db/:example/api-serverandexample/lambda-workergenerate typed query code with sqlc into a shareddb/package, configured bysqlc.yamlin the project root. The schema isdb/schema.sql; the queries aredb/queries/*.sqlin api-server anddb/orders.sqlin lambda-worker.templates/andstatic/:example/templates-basickeeps app-wide HTML templates (withlayouts/andpartials/) and static assets at the root, and loads templates withos.DirFS("templates"). See Templates and UI.