Simple guides introducing Fabrica with hands-on examples.
Understanding the framework’s design, components, and extension points.
Fabrica is a framework for building resource-based REST APIs with automatic code generation. It follows the Kubernetes resource pattern and emphasizes:
Building REST APIs involves repetitive boilerplate:
Result: 90% boilerplate, 10% business logic.
Define your resource once, generate everything else:
Resource Definition (100 lines)
↓
Code Generator
↓
Generated Code (2000+ lines)
├─ REST API handlers
├─ Storage operations
├─ Client library
├─ OpenAPI spec
└─ CLI commands
Result: Focus on business logic, not plumbing.
Perfect for:
Not ideal for:
Follow proven patterns from Kubernetes:
type Resource struct {
APIVersion string // Version of the API
Kind string // Type of resource
Metadata Metadata // Standard metadata
Spec T // Desired state
Status U // Observed state
}
Why? Kubernetes patterns are battle-tested and familiar.
Generate consistent code from templates:
Templates (Manual)
↓
Generator Engine
↓
Generated Code (Automatic)
Why? One source of truth, applied everywhere.
Compile-time checking across the stack:
// Server side
func CreateDevice(device *Device) error { ... }
// Storage layer
storage.Save(ctx, device) // Type-checked
// Client side
client.CreateDevice(ctx, device) // Type-checked
Why? Catch errors at compile time, not runtime.
Interface-based design for flexibility:
Why? Adapt to your needs without framework changes.
Start simple, add features as needed:
1. Define resource → Basic CRUD
2. Add labels → Query and filter
3. Add authorization → Access control
4. Add versioning → Compatibility
5. Add custom storage → Scale
Why? Don’t pay for features you don’t use.
┌─────────────────────────────────────────────────────┐
│ HTTP Layer │
│ ┌────────────────────────────────────────────┐ │
│ │ Generated REST API Handlers │ │
│ │ (List, Get, Create, Update, Delete) │ │
│ └────────────────┬───────────────────────────┘ │
└───────────────────┼────────────────────────────────┘
│
┌───────────────────▼────────────────────────────────┐
│ Framework Layer │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ Versioning │ │ Authorization│ │ Validation │ │
│ │ Registry │ │ Policies │ │ Rules │ │
│ └─────────────┘ └──────────────┘ └─────────────┘ │
└───────────────────┬────────────────────────────────┘
│
┌───────────────────▼────────────────────────────────┐
│ Storage Layer │
│ ┌────────────────────────────────────────────┐ │
│ │ Storage Backend Interface │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ File │ │ Database │ │ Custom │ │ │
│ │ │ Backend │ │ Backend │ │ Backend │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
Layer 1: HTTP Layer
Layer 2: Framework Layer
Layer 3: Storage Layer
pkg/resource)Purpose: Define resource structure and common operations.
Key Components:
Resource struct - Base resource typeMetadata - Name, UID, labels, annotations, timestampsConditions - Status conditions patternExample:
type Device struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Metadata Metadata `json:"metadata"`
Spec DeviceSpec `json:"spec"`
Status DeviceStatus `json:"status,omitempty"`
}
pkg/codegen)Purpose: Generate consistent code from resource definitions.
Key Components:
Generator - Main code generation engineResourceMetadata - Extracted resource informationTemplates - Go text templatesFlow:
Resource Definition
↓
Reflection (extract metadata)
↓
Template Application
↓
Go code formatting
↓
File writing
pkg/storage)Purpose: Pluggable persistence layer.
Key Components:
StorageBackend interface - Core operationsFileBackend - File-based implementationResourceStorage[T] - Type-safe wrapperOperations:
backend.LoadAll(ctx, "Device") // List all
backend.Load(ctx, "Device", uid) // Get one
backend.Save(ctx, "Device", uid, data) // Create/Update
backend.Delete(ctx, "Device", uid) // Delete
backend.Exists(ctx, "Device", uid) // Check existence
pkg/versioning)Purpose: Multi-version schema support.
Key Components:
VersionRegistry - Register and lookup versionsSchemaVersion - Version metadataVersionConverter - Convert between versionsFlow:
Client Request (v1)
↓
Version Registry (lookup v1)
↓
Storage (load v2)
↓
Converter (v2 → v1)
↓
Response (v1)
Purpose: Flexible access control through custom middleware.
Key Points:
Example Authentication Middleware:
// internal/middleware/auth.go
package middleware
import (
"context"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v5"
)
func AuthMiddleware(jwtSecret []byte) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil || !token.Valid {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Add claims to context for downstream handlers
ctx := context.WithValue(r.Context(), "user", token.Claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
Integration:
// cmd/server/main.go
func main() {
router := chi.NewRouter()
// Add your auth middleware
router.Use(AuthMiddleware(jwtSecret))
// Register generated routes
RegisterRoutes(router, storage, eventBus)
http.ListenAndServe(":8080", router)
}
See Also:
1. HTTP POST /devices
↓
2. Auth Middleware (if configured)
↓
3. Generated Handler: CreateDevice()
↓
4. Validation Middleware
↓
5. Generate UID: dev-1a2b3c4d
↓
6. Set Timestamps: CreatedAt, UpdatedAt
↓
7. Storage: backend.Save()
↓
8. Event Publishing (if enabled)
↓
9. Response: 201 Created with resource
1. HTTP GET /devices/dev-123
↓
2. Auth Middleware (if configured)
↓
3. Generated Handler: GetDevice()
↓
4. Version Negotiation: Body `apiVersion`, URL version, or Accept header
↓
5. Storage: backend.Load()
↓
6. Version Conversion: v2 → v1 (if needed)
↓
7. Conditional Middleware: Check ETags
↓
8. Response: 200 OK with resource
1. HTTP GET /devices
↓
2. Auth Middleware (if configured)
↓
3. Generated Handler: ListDevices()
↓
4. Storage: backend.LoadAll()
↓
5. Label Filtering: (if query params)
↓
6. Version Conversion: (if needed)
↓
7. Response: 200 OK with array
Implement StorageBackend interface:
type PostgresBackend struct {
db *sql.DB
}
func (b *PostgresBackend) Load(ctx context.Context, resourceType, uid string) (json.RawMessage, error) {
var data json.RawMessage
err := b.db.QueryRowContext(ctx,
"SELECT data FROM resources WHERE type=$1 AND uid=$2",
resourceType, uid,
).Scan(&data)
return data, err
}
// Implement other methods...
Add authentication or authorization as middleware:
// internal/middleware/tenant.go
package middleware
import (
"context"
"net/http"
)
type TenantKey string
const TenantContextKey = TenantKey("tenant")
func MultiTenantMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Extract tenant from JWT claims (assumed set by auth middleware)
user, _ := r.Context().Value("user").(map[string]interface{})
tenantID, _ := user["tenant_id"].(string)
if tenantID == "" {
http.Error(w, "Missing tenant information", http.StatusBadRequest)
return
}
// Add tenant to context for handlers
ctx := context.WithValue(r.Context(), TenantContextKey, tenantID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Usage in handlers:
// Filter resources by tenant
func (h *DeviceHandler) ListDevices(w http.ResponseWriter, r *http.Request) {
tenantID := r.Context().Value(middleware.TenantContextKey).(string)
devices, _ := h.Storage.LoadAll(r.Context(), "Device")
// Filter by tenant label
filtered := []Device{}
for _, device := range devices {
if device.Metadata.Labels["tenant"] == tenantID {
filtered = append(filtered, device)
}
}
json.NewEncoder(w).Encode(filtered)
}
See: Middleware Customization Guide
Add functions to code generator:
generator.Templates["handlers"].Funcs(template.FuncMap{
"snakeCase": func(s string) string {
// Convert to snake_case
return strings.ToLower(regexp.MustCompile(`([A-Z])`).
ReplaceAllString(s, "_$1"))
},
})
Add middleware to generated routes:
// In your main.go
func main() {
backend := storage.NewFileBackend("./data")
// Register routes
RegisterRoutes(backend)
// Add middleware
handler := loggingMiddleware(
authMiddleware(
http.DefaultServeMux,
),
)
http.ListenAndServe(":8080", handler)
}
Add validation to resource:
type Device struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Metadata Metadata `json:"metadata"`
Spec DeviceSpec `json:"spec"`
}
func (d *Device) Validate() error {
if d.Spec.Name == "" {
return fmt.Errorf("name is required")
}
if d.Spec.Type != "sensor" && d.Spec.Type != "actuator" {
return fmt.Errorf("invalid device type")
}
return nil
}
Call in handler:
func CreateDevice(device *Device) error {
if err := device.Validate(); err != nil {
return err
}
// Continue with save...
}
DO:
DON’T:
DO:
DON’T:
DO:
DON’T:
DO:
DON’T:
DO:
DON’T:
Fabrica provides:
Next Steps:
| Questions? Open an Issue | Want to contribute? Contributing Guide |