Skip to content

Getting Started

This guide builds the smallest useful Anvil HTTP backend: one controller, one dependency, generated wiring, and a standard-library HTTP driver.

  • Go 1.26.3 or newer
  • The Anvil CLI installed with:
Terminal window
go install github.com/TDB-Group/anvil/cmd/anvil@latest
Terminal window
mkdir portal
cd portal
go mod init example.com/portal

Add Anvil and a driver:

Terminal window
go get github.com/TDB-Group/anvil@latest
go get github.com/TDB-Group/anvil-http/std@latest

Create api/projects.go:

package api
import (
"net/http"
"github.com/TDB-Group/anvil/sdk"
"example.com/portal/internal/project"
)
type Projects struct {
sdk.Controller `path:"/projects"`
Store project.Store `inject:""`
Routes struct {
Create sdk.POST `path:"/"`
Get sdk.GET `path:"/:projectId"`
}
}
type CreateProjectRequest struct {
Name string `json:"name" validate:"required,min=2,max=80"`
}
type GetProjectRequest struct {
ProjectID project.ID `param:"projectId" validate:"required"`
}
func (p *Projects) Create(ctx sdk.Ctx, req CreateProjectRequest) (project.DTO, error) {
created, err := p.Store.Create(ctx.Context(), req.Name)
if err != nil {
return project.DTO{}, ctx.Errors().Wrap(err, "creating project")
}
ctx.Response().Status(http.StatusCreated)
ctx.Response().Header("Location", "/projects/"+created.ID.String())
return project.ToDTO(created), nil
}
func (p *Projects) Get(ctx sdk.Ctx, req GetProjectRequest) (project.DTO, error) {
found, err := p.Store.Find(ctx.Context(), req.ProjectID)
if project.IsNotFound(err) {
return project.DTO{}, ctx.Errors().NotFound("project")
}
if err != nil {
return project.DTO{}, ctx.Errors().Wrap(err, "querying project")
}
return project.ToDTO(found), nil
}

The route table declares the HTTP surface. Handlers stay focused on application work, while the request model defines binding and validation.

Create anvil.yaml:

project:
name: portal
main: ./cmd/server
scan:
dirs:
- ./api
generate:
output: ./anvilgen/anvil_autogenerated.go
package: anvilgen
commands:
generate:
argv: ["anvil", "generate", "."]
test:
argv: ["go", "test", "./..."]

Create cmd/server/main.go:

package main
import (
"log"
anvil "github.com/TDB-Group/anvil"
httpstd "github.com/TDB-Group/anvil-http/std"
"example.com/portal/internal/project"
_ "example.com/portal/anvilgen"
)
func main() {
app := anvil.New(
httpstd.Driver(),
anvil.WithProviders(
anvil.As[project.Store](project.NewMemoryStore()),
),
)
if err := app.Wire(); err != nil {
log.Fatal(err)
}
if err := app.Listen(anvil.Env("ADDR", ":3000")); err != nil {
log.Fatal(err)
}
}

Generated code can be blank-imported. The blank import registers generated wiring through init; the app still chooses the driver and dependencies.

Terminal window
anvil generate .
go test ./...
go run ./cmd/server

Anvil generates anvilgen/anvil_autogenerated.go. That file is regular Go code, so you can inspect it when debugging.

Anvil scanned ./api, found sdk.Controller, read the route table, validated the handlers, generated providers, generated binding and validation code, and registered routes against the installed HTTP driver.

Route discovery already happened during generation. Request handling uses the generated route table.