Foundation v2 Project Shape
A Foundation service should expose its wiring in a small number of predictable
files. The static registry is generated from types, so route and action ownership
stays next to the code that implements it.
Layout
cmd/api/main.go
internal/config/config.go
internal/contracts/users.go
internal/handlers/users.go
internal/actions/users.go
internal/services/users.go
internal/jobs/cleanup.go
internal/testing/host.go
internal/handlers/zz_foundation.gen.go
internal/actions/zz_foundation.gen.go
cmd/api/main.go owns process startup. internal/contracts contains interfaces
shared across application packages. Handlers and actions declare Foundation
metadata on their concrete types. Services implement contracts with explicit
compiler-visible declarations.
Each package containing handlers or actions gets its own generated file. Do not
edit generated files.
Startup
package main
import (
"log"
"example.com/service/internal"
)
func main() {
application, err := internal.Build()
if err != nil {
log.Fatal(err)
}
if err := application.Listen(":8080"); err != nil {
log.Fatal(err)
}
}
Application wiring stays in the package that owns generated registration:
func Build() (*app.App, error) {
application := app.New().
Provide("users", services.NewUsers())
handlers.RegisterFoundation(application)
useractions.RegisterFoundation(application)
if _, err := application.Build(); err != nil {
return nil, err
}
return application, nil
}
Contracts
Use a marker when the generator should own the assertion:
type Users interface {
Find(context.Context, int) (User, error)
}
type UserService struct {
contracts.Implements[Users]
}
Use contracts.Assert for an explicit assertion without generation:
var _ = contracts.Assert[Users]((*UserService)(nil))
Both patterns fail during development. The marker also gives the Foundation
analyzer and editor a stable relationship to display.
HTTP handlers
type GetUser struct {
_ struct{} `method:"GET" path:"/users/{id:int}"`
ID int `path:"id"`
Users Users `inject:"users"`
}
func (h *GetUser) Handle(ctx context.Context) (any, error) {
return h.Users.Find(ctx, h.ID)
}
The analyzer checks method and path syntax, parameter fields, duplicate routes,
and local named dependency types. The generated registry supplies a static
constructor. App.Build validates dependency availability before serving.
Actions
type CreateUser struct {
_ struct{} `action:"users.create" keys:"ctrl+n"`
Name string `json:"name"`
Users Users `inject:"users"`
}
func (a *CreateUser) Handle(ctx context.Context) (any, error) {
return a.Users.Create(ctx, a.Name)
}
Use typed actions when the caller and handler are both Go code and no string
metadata is required.
Testing
Use app/testing for host-level tests and the focused package APIs for unit
tests:
func newTestHost() *apptest.TestHost {
return apptest.NewTestHost(func(builder *di.Builder) {
builder.Provide("users", newFakeUsers())
}, func(server *web.Server, container *di.Container) error {
return server.RegisterDefinition(handlerDefinition, container)
})
}
Run the same static steps locally and in CI:
foundation generate -check ./...
foundation check ./...
go test -race ./...
go vet ./...
Reading order
New contributors should be able to understand a service in this order:
cmd/api/main.go
- application
Build
- contracts
- handlers and actions
- services and jobs
- tests
If routes, dependencies, or registrations require a runtime trace to discover,
move that relationship into a contract, typed key, or generated registry.