Simple guides introducing Fabrica with hands-on examples.
Fabrica uses template-based code generation to create consistent, production-ready code for REST APIs, storage backends, and client libraries. This document explains how the code generation system works and how to use it effectively.
Fabrica uses a two-phase code generation approach:
fabrica codegen init)apis/<group>/<version>/ for resource definitions using AST parsing driven by apis.yamlapis/<group>/<version>/register_generated.go with importsfabrica generate)go fmt on all generated filesAll generated files have the suffix _generated.go and include a header comment warning not to edit them directly.
Why two phases? Go doesn’t support dynamic type loading at runtime. The registration file bridges the gap by importing and instantiating resource types, which the generator then introspects via reflection.
# Two-phase workflow:
# 1. Initialize code generation (after adding/modifying resources)
fabrica codegen init
# 2. Generate everything (recommended)
fabrica generate
# Or generate specific components
fabrica generate --handlers # Just HTTP handlers
fabrica generate --storage # Just storage layer
fabrica generate --client # Just client library
fabrica generate --openapi # Just OpenAPI spec
# Or use the Makefile for the complete workflow
make dev # Clean, init, generate, and build
The code generator consists of several key parts:
//go:embedThe generator automatically discovers resources using Go’s AST parser:
// Scans apis/<group>/<version>/ and looks for:
type MyResource struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Metadata resource.Metadata `json:"metadata"`
Spec MyResourceSpec `json:"spec"`
Status MyResourceStatus `json:"status,omitempty"`
}
Discovery process:
apis.yaml to get groups, versions, and resourcesapis/<group>/<version>/ directory tree_types.go file into an ASTAll templates are located in pkg/codegen/templates/:
| Template | Purpose | Output Location | Used By |
|---|---|---|---|
handlers.go.tmpl |
REST API CRUD handlers | cmd/server/*_handlers_generated.go |
Server |
storage.go.tmpl |
File-based storage operations | internal/storage/storage_generated.go |
Server (file backend) |
storage_ent.go.tmpl |
Ent database storage operations | internal/storage/storage_generated.go |
Server (ent backend) |
routes.go.tmpl |
HTTP route registration | cmd/server/routes_generated.go |
Server |
models.go.tmpl |
Request/response types | cmd/server/models_generated.go |
Server |
openapi.go.tmpl |
OpenAPI 3.0 specification | cmd/server/openapi_generated.go |
Server |
client.go.tmpl |
HTTP client library | pkg/client/client_generated.go |
Client |
client-models.go.tmpl |
Client-side types | pkg/client/models_generated.go |
Client |
client-cmd.go.tmpl |
CLI application (Cobra-based) | cmd/cli/main_generated.go |
CLI |
reconciler.go.tmpl |
Resource reconciliation logic | pkg/reconcile/*_reconciler_generated.go |
Reconcile |
reconciler-registration.go.tmpl |
Reconciler registration | pkg/reconcile/registration_generated.go |
Reconcile |
event-handlers.go.tmpl |
Cross-resource event handlers | pkg/reconcile/event_handlers_generated.go |
Reconcile |
When using Ent storage (--storage ent), additional templates are used:
| Template | Purpose | Output Location |
|---|---|---|
ent/schema/resource.go.tmpl |
Generic resource schema | internal/storage/ent/schema/resource.go |
ent/schema/label.go.tmpl |
Resource label schema | internal/storage/ent/schema/label.go |
ent/schema/annotation.go.tmpl |
Resource annotation schema | internal/storage/ent/schema/annotation.go |
ent_adapter.go.tmpl |
Adapter between Fabrica and Ent | internal/storage/ent_adapter.go |
generate.go.tmpl |
Ent code generation directive | internal/storage/generate.go |
Fabrica generates several middleware templates for common functionality:
| Template | Purpose | Output Location |
|---|---|---|
validation_middleware.go.tmpl |
Request validation | internal/middleware/validation_middleware_generated.go |
versioning_middleware.go.tmpl |
API versioning | internal/middleware/versioning_middleware_generated.go |
conditional_middleware.go.tmpl |
Conditional requests (ETags) | internal/middleware/conditional_middleware_generated.go |
For custom authorization, implement your own middleware in internal/middleware/.
Templates have access to resource metadata:
type ResourceMetadata struct {
Name string // "Device"
PluralName string // "devices"
Package string // "github.com/user/project/apis/example.fabrica.dev/v1"
PackageAlias string // "device"
TypeName string // "*device.Device"
SpecType string // "device.DeviceSpec"
StatusType string // "device.DeviceStatus"
URLPath string // "/devices"
StorageName string // "Device"
}
Template usage example:
// In handlers.go.tmpl
func Get(c fuego.ContextWithBody[GetRequest]) (, error) {
id := c.PathParam("id")
return storage.Load(c.Context(), id)
}
Generates:
// In device_handlers_generated.go
func GetDevice(c fuego.ContextWithBody[GetRequest]) (*device.Device, error) {
id := c.PathParam("id")
return storage.LoadDevice(c.Context(), id)
}
Templates can use these helper functions:
| Function | Purpose | Example |
|---|---|---|
toLower |
Convert to lowercase | `` → device |
toUpper |
Convert to uppercase | `` → DEVICE |
title |
Capitalize first letter | `` → Devices |
camelCase |
Convert to camelCase | `` → device |
trimPrefix |
Remove prefix | `` → 1 |
The generator operates in three modes based on the PackageName:
PackageName: "main")Generates complete server-side code:
GenerateHandlers() - REST API endpointsGenerateStorage() - Data persistence layerGenerateRoutes() - URL routing configurationGenerateModels() - Request/response typesGenerateOpenAPI() - OpenAPI specificationGenerateMiddleware() - Validation, versioning, conditional requestsOutput: Files in cmd/server/, internal/storage/, and internal/middleware/
PackageName: "client")Generates client library code:
GenerateClient() - HTTP client with CRUD methodsGenerateClientModels() - Client-side data typesOutput: Files in pkg/client/
PackageName: "reconcile")Generates reconciliation code for eventual consistency:
GenerateReconcilers() - Resource reconciliation logicGenerateReconcilerRegistration() - Registration codeGenerateEventHandlers() - Cross-resource event handlingOutput: Files in pkg/reconcile/
The generator adapts output based on storage type:
gen := codegen.NewGenerator(outputDir, "main", modulePath)
gen.SetStorageType("file") // or omit, it's the default
gen.GenerateStorage()
Uses: storage.go.tmpl
Creates: JSON file-based persistence in ./data/
gen := codegen.NewGenerator(outputDir, "main", modulePath)
gen.SetStorageType("ent")
gen.SetDBDriver("postgres") // or "mysql", "sqlite"
gen.GenerateStorage()
gen.GenerateEntSchemas()
gen.GenerateEntAdapter()
Uses: storage_ent.go.tmpl + Ent templates
Creates: Database-backed storage with migrations
Templates are embedded in the binary using Go’s embed directive:
// pkg/codegen/generator.go
//go:embed templates/*
var embeddedTemplates embed.FS
Why this matters:
go install (templates travel with binary)../../templates)func (g *Generator) LoadTemplates() error {
// Read from embedded filesystem (not disk!)
content, err := embeddedTemplates.ReadFile("templates/handlers.go.tmpl")
// Parse with helper functions
tmpl, err := template.New("handlers").Funcs(templateFuncs).Parse(string(content))
g.Templates["handlers"] = tmpl
return nil
}
func (g *Generator) GenerateHandlers() error {
for _, resource := range g.Resources {
var buf bytes.Buffer
// Execute template with resource metadata
err := g.Templates["handlers"].Execute(&buf, resource)
// Format with go fmt
formatted, err := format.Source(buf.Bytes())
// Write to file
filename := fmt.Sprintf("%s_handlers_generated.go", strings.ToLower(resource.Name))
os.WriteFile(filepath.Join(g.OutputDir, filename), formatted, 0644)
}
return nil
}
Resources are registered using a two-phase approach:
Phase 1: Generate Registration File
fabrica codegen init
This scans apis/<group>/<version>/ (as listed in apis.yaml) and creates apis/<group>/<version>/register_generated.go:
// Code generated by fabrica codegen init. DO NOT EDIT.
package v1
import (
"fmt"
"github.com/openchami/fabrica/pkg/codegen"
"github.com/user/project/apis/example.fabrica.dev/v1"
)
func RegisterAllResources(gen *codegen.Generator) error {
if err := gen.RegisterResource(&v1.Device{}); err != nil {
return fmt.Errorf("failed to register Device: %w", err)
}
return nil
}
Phase 2: Use Registration for Generation
When you run fabrica generate, it:
apis/RegisterAllResources() to register discovered types via reflectionThe generated Makefile provides convenient targets for development:
# Complete development workflow (clean, init, generate, build)
make dev
# Individual targets
make codegen-init # Initialize code generation
make generate # Generate handlers, storage, and OpenAPI
make build # Build the server
make run # Build and run the server
make test # Run tests
make clean # Remove all generated files and binaries
What make dev does:
make clean)fabrica codegen init)fabrica generate --handlers --storage --openapi)This is the recommended workflow when adding or modifying resources.
# 1. Create resource definition
fabrica add resource Product
# 2. Customize the resource
vim apis/example.fabrica.dev/v1/product_types.go
# 3. Initialize code generation (register the new resource)
fabrica codegen init
# 4. Generate code
fabrica generate
# 5. Build and run
go build -o bin/server cmd/server/*.go
./bin/server
# Or use the Makefile for steps 3-5:
make dev
Don’t edit generated files directly! Instead:
# 1. Find the template
# Generated file says: "Generated from: pkg/codegen/templates/handlers.go.tmpl"
# 2. Edit the template
vim pkg/codegen/templates/handlers.go.tmpl
# 3. Regenerate
fabrica generate
# 4. Verify changes
git diff cmd/server/
Example: Add a count endpoint for each resource
Edit handlers.go.tmpl:
// Add this function to the template
func GetCount(c fuego.ContextNoBody) (map[string]int, error) {
resources, err := storage.LoadAlls(c.Context())
if err != nil {
return nil, err
}
return map[string]int{"count": len(resources)}, nil
}
Edit routes.go.tmpl:
// Add this route registration
fuego.Get(server, "/count", GetCount)
Regenerate:
fabrica generate
Result: Every resource now has a /resources/count endpoint!
From file to database:
# 1. Regenerate with ent storage
# (Edit your project's generation code to set storage type)
# 2. Create database
createdb myapp
# 3. Generate Ent schemas
fabrica generate
# 4. Run migrations
cd internal/storage && go generate ./ent
# 5. Update main.go to use Ent backend
After running fabrica codegen init and fabrica generate on a project with a Device resource:
myproject/
├── cmd/server/
│ ├── main.go # Server entry point (user-maintained)
│ ├── device_handlers_generated.go # CRUD handlers for Device
│ ├── routes_generated.go # Route registration
│ ├── models_generated.go # Request/response types + helpers
│ └── openapi_generated.go # OpenAPI spec
├── internal/storage/
│ └── storage_generated.go # Storage wrappers using fabrica/pkg/storage
├── pkg/client/
│ ├── client_generated.go # HTTP client
│ └── models_generated.go # Client types
├── apis/
│ └── example.fabrica.dev/
│ └── v1/
│ ├── register_generated.go # Resource registration (from codegen init)
│ └── device/
│ └── device.go # Resource definition (user-maintained)
└── Makefile # Build automation with dev workflow
Resources can have multiple schema versions:
gen := codegen.NewGenerator("cmd/server", "main", modulePath)
gen.RegisterResource(&device.Device{})
// Add a new version
gen.AddResourceVersion("Device", codegen.SchemaVersion{
Version: "v2",
IsDefault: false,
Stability: "beta",
Deprecated: false,
SpecType: "device.DeviceV2Spec",
})
Add custom authentication/authorization middleware:
// In internal/middleware/auth.go
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Implement your auth logic
next.ServeHTTP(w, r)
})
}
Apply middleware in your routes or main.go.
Extend template capabilities:
// In pkg/codegen/generator.go
var templateFuncs = template.FuncMap{
"toLower": strings.ToLower,
"myCustomFunc": func(s string) string {
// Your logic here
},
}
Error: failed to read embedded template templates/handlers.go.tmpl: no such file or directory
Cause: Templates not embedded properly (usually during development)
Fix:
pkg/codegen/templates///go:embed templates/* directive existsgo build -o bin/fabrica cmd/fabrica/*.goError: No resources found in apis/<group>/<version>/
Cause: Resource doesn’t have flattened envelope structure or file doesn’t parse
Fix:
// Make sure your resource looks like this:
type MyResource struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Metadata Metadata `json:"metadata"`
Spec MyResourceSpec `json:"spec"`
Status MyResourceStatus `json:"status,omitempty"`
}
Error: registration file not found: run 'fabrica codegen init' first
Cause: You ran fabrica generate without first running fabrica codegen init
Fix:
# Run codegen init to create the registration file
fabrica codegen init
# Then generate code
fabrica generate
# Or use the Makefile which does both
make dev
Error: undefined: device.Device
Cause: Missing import or incorrect package path
Fix:
go mod tidygo.mod module path matches template’s ModulePathError: failed to format generated code: expected ';', found '}'
Cause: Template produces invalid Go syntax
Fix:
go run cmd/fabrica/main.go generateformat.Source() temporarily)/devices, /products)validate:"required,email"# 1. Generate code
fabrica generate
# 2. Verify it compiles
go build ./cmd/server
# 3. Run tests
go test ./...
# 4. Check formatting
go fmt ./...
# 5. Lint
golangci-lint run
Instead of editing generated files:
main.goTemplates are parsed once during LoadTemplates():
For a project with 10 resources:
Generation is fast enough to run on every code change.
Generated code size (per resource):
This is acceptable for generated code and provides good readability.
To improve code generation:
.tmpl fileFabrica’s code generation system:
go fmtmake dev workflowThe result: Write resource definitions once, get complete CRUD APIs automatically.
✅ Working:
Note: The core infrastructure is complete and production-ready. For authentication/authorization, implement custom middleware in internal/middleware/.