| title | Code Generation |
|---|---|
| weight | 3 |
| description | Complete guide to Loom's code generation - commands, process, generated code structure, and customization options. |
| llm_optimized | true |
| aliases |
Loom's code generation transforms your design into production-ready service
interfaces, endpoints, transport adapters, clients, and API contracts. The
loom example command can scaffold runnable starter implementations, but the
business logic stays in files you own.
go install github.com/CaliLuke/loom/cmd/loom@latestAll commands expect Go package import paths, not filesystem paths:
loom gen github.com/CaliLuke/loom-examples/calc/designloom gen <design-package-import-path> [-o <output-dir>] [--debug]The primary command for code generation:
- Processes your design package and generates implementation code
- Generates and finalizes a complete staging tree, validates its manifest and
outputs, then replaces the entire
gen/directory and any declared plugin outputs on success. A generation, finalization, validation, or installation failure restores the previous generated artifacts. - Run after every design change
loom example <design-package-import-path> [-o <output-dir>] [--debug]A scaffolding command:
- Creates a one-time example implementation
- Generates handler stubs with example logic
- Run once when starting a new project
- Will NOT overwrite existing custom implementation
loom version- Create initial design
- Run
loom gento generate base code - Run
loom exampleto create implementation stubs - Implement your service logic
- Run
loom genafter every design change
Best Practice: Commit generated code to version control rather than generating during CI/CD. This ensures reproducible builds and allows tracking changes in generated code.
When you run loom gen, Loom follows a systematic process:
Loom creates a temporary main.go that:
- Imports Loom packages and your design package
- Runs DSL evaluation
- Triggers code generation
- DSL functions execute to create expression objects
- Expressions combine into a complete API model
- Relationships between expressions are established
- Design rules and constraints are validated
- Validated expressions pass to code generators
- Templates render to produce code files
- Output writes to the
gen/directory
A typical project after loom gen and loom example depends on the transports
used in the design. A service with HTTP and gRPC enabled looks like this:
myservice/
├── cmd/ # Example commands you can customize
│ └── calc/
│ ├── grpc.go
│ └── http.go
├── design/ # Your design files
│ └── design.go
├── gen/ # Generated code (don't edit)
│ ├── calc/ # Service-specific code
│ │ ├── client.go
│ │ ├── endpoints.go
│ │ └── service.go
│ ├── http/ # HTTP transport layer
│ │ ├── calc/
│ │ │ ├── client/
│ │ │ └── server/
│ │ ├── openapi.json
│ │ └── openapi.yaml
│ └── grpc/ # gRPC transport layer
│ └── calc/
│ ├── client/
│ ├── server/
│ └── pb/
└── myservice.go # Your service implementation
Small HTTP and JSON-RPC services keep compact types.go files. When a
generated transport package grows beyond the type-section split threshold,
Loom writes deterministic concern files such as types_requests.go,
types_responses.go, types_unions.go, types_validation.go, and
types_helpers.go in the same package. The exported Go API is unchanged; the
split only keeps large generated packages navigable and diff-friendly.
Large generated HTTP and JSON-RPC clients also expose deterministic operation
groups derived from the first route path segment, for example
client.Items.List() and client.Items.BuildListRequest(...). The flat
methods remain available on Client, so existing consumers keep compiling
while larger services gain a narrower navigation surface.
Generated in gen/<service>/service.go:
// Service interface defines the API contract
type Service interface {
Add(context.Context, *AddPayload) (res int, err error)
Multiply(context.Context, *MultiplyPayload) (res int, err error)
}
// Payload types
type AddPayload struct {
A int32
B int32
}
// Constants for observability
const ServiceName = "calc"
var MethodNames = [2]string{"add", "multiply"}Generated in gen/<service>/endpoints.go:
// Endpoints wraps service methods in transport-agnostic endpoints
type Endpoints struct {
Add loom.Endpoint
Multiply loom.Endpoint
}
// NewEndpoints creates endpoints from service implementation
func NewEndpoints(s Service) *Endpoints {
return &Endpoints{
Add: NewAddEndpoint(s),
Multiply: NewMultiplyEndpoint(s),
}
}
// Use applies middleware to all endpoints
func (e *Endpoints) Use(m func(loom.Endpoint) loom.Endpoint) {
e.Add = m(e.Add)
e.Multiply = m(e.Multiply)
}Endpoint middleware example:
func LoggingMiddleware(next loom.Endpoint) loom.Endpoint {
return func(ctx context.Context, req any) (res any, err error) {
log.Printf("request: %v", req)
res, err = next(ctx, req)
log.Printf("response: %v", res)
return
}
}
endpoints.Use(LoggingMiddleware)Generated in gen/<service>/client.go:
// Client provides typed methods for service calls
type Client struct {
AddEndpoint loom.Endpoint
MultiplyEndpoint loom.Endpoint
}
func NewClient(add, multiply loom.Endpoint) *Client {
return &Client{
AddEndpoint: add,
MultiplyEndpoint: multiply,
}
}
func (c *Client) Add(ctx context.Context, p *AddPayload) (res int, err error) {
ires, err := c.AddEndpoint(ctx, p)
if err != nil {
return
}
return ires.(int), nil
}Generated in gen/http/<service>/server/server.go:
func New(
e *calc.Endpoints,
mux loomhttp.Muxer,
decoder func(*http.Request) loomhttp.Decoder,
encoder func(context.Context, http.ResponseWriter) loomhttp.Encoder,
errhandler func(context.Context, http.ResponseWriter, error),
formatter func(ctx context.Context, err error) loomhttp.Statuser,
) *Server
// Server exposes handlers for modification
type Server struct {
Mounts []*MountPoint
Add http.Handler
Multiply http.Handler
}
// Use applies HTTP middleware to all handlers
func (s *Server) Use(m func(http.Handler) http.Handler)Complete server setup:
func main() {
svc := calc.New()
endpoints := gencalc.NewEndpoints(svc)
mux := loomhttp.NewMuxer()
server := genhttp.New(
endpoints,
mux,
loomhttp.RequestDecoder,
loomhttp.ResponseEncoder,
nil, nil)
genhttp.Mount(mux, server)
http.ListenAndServe(":8080", mux)
}Generated in gen/http/<service>/client/client.go:
func NewClient(
scheme string,
host string,
doer loomhttp.Doer,
enc func(*http.Request) loomhttp.Encoder,
dec func(*http.Response) loomhttp.Decoder,
restoreBody bool,
) *ClientComplete client setup:
func main() {
httpClient := genclient.NewClient(
"http",
"localhost:8080",
http.DefaultClient,
loomhttp.RequestEncoder,
loomhttp.ResponseDecoder,
false,
)
client := gencalc.NewClient(
httpClient.Add(),
httpClient.Multiply(),
)
result, err := client.Add(context.Background(), &gencalc.AddPayload{A: 1, B: 2})
}HTTP generation also emits command-line client support under
gen/http/cli/<server>/cli.go and per-service payload builders under
gen/http/<service>/client/cli.go. The generated parser is Kong-backed, so
service and method descriptions from the design become command help, while
Loom-owned generated code still constructs the typed endpoint and payload.
loom example wires that support into cmd/<server>-cli so local testing can
call any generated endpoint without hand-writing a test client:
go run ./cmd/calc-cli --url=http://localhost:8080 calc add --a=1 --b=2
go run ./cmd/calc-cli calc add --helpGenerated in gen/grpc/<service>/pb/:
syntax = "proto3";
package calc;
service Calc {
rpc Add (AddRequest) returns (AddResponse);
rpc Multiply (MultiplyRequest) returns (MultiplyResponse);
}
message AddRequest {
int64 a = 1;
int64 b = 2;
}func main() {
svc := calc.New()
endpoints := gencalc.NewEndpoints(svc)
svr := grpc.NewServer()
gensvr := gengrpc.New(endpoints, nil)
genpb.RegisterCalcServer(svr, gensvr)
lis, _ := net.Listen("tcp", ":8080")
svr.Serve(lis)
}func main() {
conn, _ := grpc.Dial("localhost:8080",
grpc.WithTransportCredentials(insecure.NewCredentials()))
defer conn.Close()
grpcClient := genclient.NewClient(conn)
client := gencalc.NewClient(
grpcClient.Add(),
grpcClient.Multiply(),
)
result, _ := client.Add(context.Background(), &gencalc.AddPayload{A: 1, B: 2})
}Force generation of types not directly referenced by methods:
var MyType = Type("MyType", func() {
// Force generation in specific services
Meta("type:generate:force", "service1", "service2")
// Or force generation in all services
Meta("type:generate:force")
Attribute("name", String)
})Generate types in a shared package:
var CommonType = Type("CommonType", func() {
Meta("struct:pkg:path", "types")
Attribute("id", String)
})Creates:
gen/
└── types/
└── common_type.go
var Message = Type("Message", func() {
Attribute("id", String, func() {
// Override field name
Meta("struct:field:name", "ID")
// Add custom struct tags
Meta("struct:tag:json", "id,omitempty")
Meta("struct:tag:msgpack", "id,omitempty")
// Override type
Meta("struct:field:type", "bson.ObjectId", "github.com/globalsign/mgo/bson", "bson")
})
})var MyType = Type("MyType", func() {
// Override protobuf message name
Meta("struct:name:proto", "CustomProtoType")
Field(1, "status", Int32, func() {
// Override protobuf field type
Meta("struct:field:proto", "int32")
})
// Use Google's timestamp type
Field(2, "created_at", String, func() {
Meta("struct:field:proto",
"google.protobuf.Timestamp",
"google/protobuf/timestamp.proto",
"Timestamp",
"google.golang.org/protobuf/types/known/timestamppb")
})
})
// Specify protoc include paths
var _ = API("calc", func() {
Meta("protoc:include", "/usr/include", "/usr/local/include")
})
// Override the protoc command for one gRPC service
var _ = Service("calc", func() {
Meta("protoc:cmd", "/usr/bin/protoc", "--fatal_warnings")
})var _ = API("MyAPI", func() {
// Control generation
Meta("openapi:generate", "false")
// Format JSON output
Meta("openapi:json:prefix", " ")
Meta("openapi:json:indent", " ")
// Disable example generation
Meta("openapi:example", "false")
})
var _ = Service("UserService", func() {
// Add tags
HTTP(func() {
Meta("openapi:tag:Users")
Meta("openapi:tag:Backend:desc", "Backend API Operations")
})
Method("CreateUser", func() {
// Custom operation ID
Meta("openapi:operationId", "{service}.{method}")
// Custom summary
Meta("openapi:summary", "Create a new user")
HTTP(func() {
// Add extensions
Meta("openapi:extension:x-rate-limit", `{"rate": 100}`)
POST("/users")
})
})
})
var User = Type("User", func() {
// Override type name in OpenAPI spec
Meta("openapi:typename", "CustomUser")
})Loom validates data at system boundaries:
- Server-side: Validates incoming requests
- Client-side: Validates incoming responses
- Internal code: Trusted to maintain invariants
| Properties | Payload/Result | Request Body (Server) | Response Body (Server) |
|---|---|---|---|
| Required OR Default | Direct (-) | Pointer (*) | Direct (-) |
| Not Required, No Default | Pointer (*) | Pointer (*) | Pointer (*) |
Special types:
- Objects (structs): Always use pointers
- Arrays and Maps: Never use pointers (already reference types)
Example:
type Person struct {
Name string // required, direct value
Age *int // optional, pointer
Hobbies []string // array, no pointer
Metadata map[string]string // map, no pointer
}- Marshaling: Default values initialize nil arrays/maps
- Unmarshaling: Default values apply to missing optional fields (not missing required fields)
Views control how result types are rendered in responses.
- Service method includes a view parameter
- A views package is generated at the service level
- View-specific validation is automatically generated
- Viewed result type is marshalled
- Nil attributes are omitted
- View name is passed in "loom-view" header
- Response is unmarshalled
- Transformed into viewed result type
- View name extracted from "loom-view" header
- View-specific validation performed
- Converted back to service result type
For ResultType and View designs, Loom generates exported projection helpers
in the service package. These helpers convert canonical result values to view
types and view values back to canonical result types. They are used by generated
wrappers such as NewViewed... and by transport encoders/decoders, which keeps
HTTP, gRPC, and JSON-RPC projections aligned with the canonical result model.
Projection generation is recursive across nested structs, slices, maps, unions,
collections, and optional fields.
View fields inherit canonical requiredness unless the view uses
ViewRequired(...) or ViewOptional(...). Those overrides drive projected
validation, HTTP field pointers and JSON tags, and OpenAPI required arrays;
nested named views retain their own requiredness contract.
If no views are defined, Loom adds a "default" view that includes all basic fields.
Loom exposes a compile-time generator plugin registry for framework extensions
such as Loom-MCP. A plugin registers from a package imported by the design
dependency graph using codegen.RegisterPluginFirst, RegisterPlugin, or
RegisterPluginLast. Each registration can provide a prepare callback that
updates evaluated roots and a generation callback that adds or transforms
files.
Generation snapshots the registry before evaluation phases run, so concurrent
registrations cannot alter an in-progress run. Plugins execute in deterministic
groups (First, normal, then Last) and by name within each group. Treat this
as a compile-time extension contract: applications should use the design DSL
and runtime middleware for application behavior, while external framework
packages use plugins only for reproducible generated artifacts.
External plugins may populate codegen.File.SectionTemplates when their
sections are template-backed. Loom-owned generators use the generic
codegen.File.Sections API, but SectionTemplates remains a supported public
extension surface and is adapted by File.AllSections during rendering.
Framework-owned HTTP, gRPC, and JSON-RPC behavior still belongs directly in the relevant generator package. Generated section hooks are an implementation mechanism inside generators, not a second plugin lifecycle.
- DSL Reference — Complete DSL reference for design files
- HTTP Guide — HTTP transport features and customization
- gRPC Guide — gRPC transport features and Protocol Buffers
- Quickstart — Getting started with code generation