November 4, 2025 in HPC, Hardware, Inventory by Ben McDonald7 minutes
Fabrica is a command-line tool designed to accelerate the development of production-ready REST APIs in Go.
At its core, Fabrica is a code generator. The primary workflow involves defining your API’s resources as simple Go structs. Once you define these structs, Fabrica generates the complete, surrounding API infrastructure. This generated code includes:
Fabrica is designed to automatically conform to the OpenCHAMI specifications, decided upon by the OpenCHAMI Technical Steering Committee and members of the API working group, so that code will automatically be OpenCHAMI compliant.
A key concept Fabrica uses is its Kubernetes-inspired resource structure. Every resource you create is wrapped in a standard “envelope” that separates the spec from the status.
spec is the desired state of the resource.status is the observed state of the resource.This separation provides a clear and consistent pattern for managing resource state, a pattern common in cloud-native tools.
To see how Fabrica works, let’s build a real-world API. We’ll use it to create an inventory-api for tracking hardware assets, based on the OpenCHAMI data model (see RFD 112).
Our goal is to create a “Device” resource. This resource needs to capture a hardware asset’s complete, observed state, including:
deviceType, manufacturer, partNumber, and serialNumber.parentID.properties field for arbitrary key-value data.To map this model to Fabrica’s spec and status pattern, because our API represents the state of hardware as it actually exists, it all belongs in the DeviceStatus. The DeviceSpec (the desired state) remains empty, as a user never requests a change to these attributes directly.
From the Fabrica root directory, run the commands to initialize your project:
fabrica init inventory-api
cd inventory-api
fabrica add resource DeviceThis creates pkg/resources/device/device.go. We open it and edit the two generated structs.
DeviceSpec
We want this struct to be empty. The system, not the user, populates the device data.
// DeviceSpec defines the desired state of a Device
// This should be empty for our inventory-API, as all data
// is observed state populated by the system.
type DeviceSpec struct {
}DeviceStatus
We fill the status struct with all the fields from our data model. This is the data our system will discover and report back to the user.
// DeviceStatus represents the observed state of a Device
type DeviceStatus struct {
// Core fields from our data model
DeviceType string `json:"deviceType,omitempty"`
Manufacturer string `json:"manufacturer,omitempty"`
PartNumber string `json:"partNumber,omitempty"`
SerialNumber string `json:"serialNumber,omitempty"`
ParentID string `json:"parentID,omitempty"`
// The arbitrary key-value store
Properties map[string]interface{} `json:"properties,omitempty"`
// A read-only list calculated by the system
ChildrenDeviceIDs []string `json:"childrenDeviceIds,omitempty"`
}You may have noticed the data model also requires standard fields like id, apiVersion, kind, createdAt, and updatedAt.
In OpenCHAMI, these fields are considered part of the standard as decided by the TSC and API working group and Fabrica will automatically generate those for you as part of the “metdata” section of the device. This means that developers only have to think about the data they actually want to store, without needing to know what is required to conform with OpenCHAMI standards.
Now that our Device resource is defined, we can generate and run the API.
Generate the code:
Run fabrica generate from the project root. Fabrica reads the structs we defined and generates all the handlers, storage, and client code.
fabrica generateInstall dependencies: Next, tidy the Go modules to pull in any new dependencies.
go mod tidyRun the server:
Finally, run the server. It will be live on localhost:8080.
go run ./cmd/serverTest the API: Step 1: Create the “Device” resource envelope. A user or system registers a new device by name and the service generates the UID.
# Create a new "Device" resource named "compute-node-01"
curl -X POST http://localhost:8080/devices \
-H "Content-Type: application/json" \
-d '{
"name": "compute-node-01",
"labels": {"role": "compute", "rack": "r10"}
}'Step 2: Simulate an external service updating the device’s status.
An inventory tool discovers the device’s properties and populates its status by making a PUT request to the /status endpoint.
# Update the status for "compute-node-01"
curl -X PUT http://localhost:8080/devices/<uid-from-last-cmd>/status \
-H "Content-Type: application/json" \
-d '{
"deviceType": "Node",
"manufacturer": "HPE",
"partNumber": "SYS-1234",
"serialNumber": "SN-ABC123",
"properties": {
"bios_boot_mode": "uefi",
"dns_domain": "cluster.local"
}
}'Step 3: Get the complete device resource. Now, when we query the device, we’ll see a device populated with the data from the previous steps:
curl http://localhost:8080/devices/<uid-from-last-cmd> | jqOutput:
{
"apiVersion": "v1",
"kind": "Device",
"schemaVersion": "v1",
"metadata": {
"name": "compute-node-01",
"uid": "dev-f63ead62",
"labels": {
"rack": "r10",
"role": "compute"
},
"createdAt": "2025-11-04T09:20:41.897902-08:00",
"updatedAt": "2025-11-04T09:27:50.092668-08:00"
},
"spec": {},
"status": {
"deviceType": "Node",
"manufacturer": "HPE",
"partNumber": "SYS-1234",
"serialNumber": "SN-ABC123",
"properties": {
"bios_boot_mode": "uefi",
"dns_domain": "cluster.local"
}
}
}What if you need an endpoint that isn’t simple CRUD? For example, what if we want a custom endpoint /devices/summary that returns a simple report, like {"total_devices": 5, "types": {"Node": 3, "Rack": 2}}?
To make this change, we can modify the file that was generated at cmd/server/main.go.
Inside your main.go file, the runServer function sets up the router. Fabrica’s generated routes are plugged in with a single call to RegisterGeneratedRoutes(r).
To add your custom endpoint, you just add your own route handler to the same router, right after Fabrica’s routes are registered.
Your runServer function already looks like this:
func runServer(cmd *cobra.Command, args []string) error {
// ... setup logging and storage ...
// Setup router
r := chi.NewRouter()
// Add middleware
r.Use(middleware.Logger)
// ... other middleware ...
// Register routes - generated by 'fabrica generate'
RegisterGeneratedRoutes(r)
r.Get("/health", healthHandler)
// ... start server ...
}To add your new endpoint, you simply add one line and one new function:
func runServer(cmd *cobra.Command, args []string) error {
// ... setup logging and storage ...
// Setup router
r := chi.NewRouter()
// Add middleware
r.Use(middleware.Logger)
// ... other middleware ...
// Register routes - generated by 'fabrica generate'
RegisterGeneratedRoutes(r)
r.Get("/health", healthHandler)
// === ADD YOUR CUSTOM ROUTE HERE ===
r.Get("/devices/summary", GetDeviceSummary)
// ==================================
// ... start server ...
}
// GetDeviceSummary is your new custom handler.
// It can re-use the generated storage logic from the
// "internal/storage" package, which is already initialized.
func GetDeviceSummary(w http.ResponseWriter, r *http.Request) {
devices, err := storage.LoadAllDevices(r.Context())
if err != nil {
// 'respondError' is a helper in main.go
respondError(w, http.StatusInternalServerError, err)
return
}
summary := map[string]interface{}{}
types := map[string]int{}
for _, dev := range devices {
types[dev.Status.DeviceType]++
}
summary["total_devices"] = len(devices)
summary["types"] = types
// 'respondJSON' is a helper in main.go
respondJSON(w, http.StatusOK, summary)
}So, using Fabrica doesn’t limit you to just CRUD operations; if you want to do something else, you just add new routes and handlers, re-using the generated storage and helper functions as needed.
So, now time to test your new function by rerunning the server and hitting the endpoint!
curl http://localhost:8080/devices/summary | jqHere we just highlighted the basic features of Fabrica and how you can extend it to do what you’d like. For more advanced features, such as reconciliation, event generation, and such, please see the main Fabrica Repository
Your feedback is valuable! If you’d like to try out this workflow, contribute ideas, or report issues, we invite you to check out the inventory API repository with a complete population script on GitHub: https://github.com/bmcdonald3/inventory.