How to Eject a Model and Customise Its Handlers
Eject when interpreter mode and hooks aren't enough — when the handler logic itself needs to change. The eject command moves the four generated Go files into custom/ so you can edit them.
When to eject
- The generated CRUD doesn't fit (you need a streaming endpoint, a bulk import, a multi-table transaction)
- You need a custom SQL query the runtime doesn't generate
- You need to change the response shape (e.g. include a computed field that's not on the Thread)
If you're "almost there" and a before_save hook would do, prefer the hook — it's far less code to maintain.
The recipe
loom weave # ensure .loom/go is fresh
loom eject Customer
What this does:
- Moves
.loom/go/models/customer.go→custom/models/customer.go - Moves
.loom/go/handlers/customer.go→custom/handlers/customer.go - Moves
.loom/go/handlers/customer_test.go→custom/handlers/customer_test.go - Moves
.loom/go/routes/customer.go→custom/routes/customer.go - Strips the
// Generated by Loomand// Source thread:header from each — the file is now yours
Re-running loom eject Customer is a no-op error unless you pass --force. That's intentional — you don't want a future loom weave to silently overwrite your customisations.
What needs to change
After ejection, the runtime no longer auto-generates these files. Two things follow:
- Update imports. The ejected handlers import
{{ .ModulePath }}/internal/authand/internal/permissions. In v0.1.0 those paths don't exist in your project — they refer to the framework's runtime. Add a thin shim or write the auth/permissions helpers yourself in your project'sinternal/auth/andinternal/permissions/. - Wire them into a custom main.go. Interpreter mode (
loom serve) doesn't consumecustom/. You need a smallmain.gothat imports your ejected routes:
import (
"github.com/jmoiron/sqlx"
"github.com/go-chi/chi/v5"
"you/custom/routes"
)
func main() {
db, _ := sqlx.Open("mysql", os.Getenv("DATABASE_URL"))
r := chi.NewRouter()
routes.MountCustomerRoutes(r, db)
http.ListenAndServe(":8080", r)
}
Editing the handler
Open custom/handlers/customer.go. The functions follow the chi http.HandlerFunc convention. To customise:
- Add fields to the JSON response by querying additional data and merging into the rendered map
- Replace the
db.SelectContextquery with a more specific one - Add a new endpoint by adding a function and registering it in
custom/routes/customer.go
Keeping schema in sync
The ejected models/customer.go is a snapshot of the Thread at eject time. If you later add a field to threads/customer.yaml, you have two options:
- Manual: add the field to the ejected struct + handler INSERT/UPDATE statements
- Diff-and-merge: run
loom weave, compare.loom/go/models/customer.goto yourcustom/models/customer.go, and apply the diff
There's no automated re-eject. Loom won't overwrite ejected files — that's the contract.
Reverting an eject
There's no loom uneject. If you decide ejection was a mistake:
- Delete the four files under
custom/ - Add the model back to interpreter-mode coverage (it always was —
loom serveignorescustom/entirely) - Drop the custom main.go
You're back to interpreter mode for that model.
See also
- Concepts → Interpreter Mode vs Ejected Code — the broader picture
- Recipes → Use Hooks to Normalize Data Before Save — what you may want instead