Use backend custom zones for business rules: validating an order, calculating a value, calling an external service or exposing a dedicated API. The generator continues to maintain the model, ordinary CRUD operations and extension contracts.
In the paths below, replace ENTITY_NAME with the generated entity directory, such as product. Replace the Go type EntityName with the real type, such as Product. These are placeholders, not filenames to create literally.
Where the code belongs
| File | Purpose |
|---|---|
backend/internal/custom/ENTITY_NAME/hooks.go |
Logic within one entity's use cases |
backend/internal/custom/app/composition.go |
Project dependencies, clients and typed hooks |
backend/internal/custom/app/routes.go |
Custom public and protected routes |
backend/internal/app/ENTITY_NAME/service.go |
Generated contract to read; implement your code in custom |
backend/internal/generated/customcontracts/ |
Types used to connect custom composition |
Change a value before saving
Open the existing hooks.go. It already declares NewHooks() and methods for the entity's enabled CRUD operations. Change the existing method body rather than adding a second declaration.
This example assumes an ordinary, non-localized string field named Name:
func (hooks) BeforeCreate(_ context.Context, value domain.EntityName) (domain.EntityName, error) {
value.Name = strings.TrimSpace(value.Name)
return value, nil
}
Use the actual entity type and add the strings import. A nullable or Localized field may be *string; handle nil first. Read the generated model instead of inferring a Go type from the UI label.
| Hook | Execution point |
|---|---|
BeforeCreate, BeforeUpdate |
Before the repository write; may change the value or return an error |
AfterCreate, AfterUpdate |
After the repository operation |
BeforeDelete |
Before deletion; receives the record key |
AfterList |
After a page of records is read |
AfterGetByID |
After an individual record is read |
The available methods depend on enabled operations. A BeforeCreate error prevents creation. An AfterCreate error does not by itself undo the completed write; operations requiring a shared transaction need an explicit custom design. Classify invalid user input through the app package's error contract, for example by wrapping ErrInvalidInput, so the HTTP boundary can return the appropriate response.
Connect an external dependency
Implement Configure(ctx, dependencies) in composition.go. The returned composition can supply typed entity hooks and protected routes. A non-nil hook replaces the legacy NewHooks() fallback; nil retains it.
dependencies.DB is the application's borrowed pool. Use it through application interfaces and do not close it. Return Cleanup(ctx) for clients you own. Configure's context is startup-only; use r.Context() for requests and a separate cancellable context, stopped by Cleanup, for your background work.
Add a custom API
Project routes use RegisterPublicRoutes and RegisterProtectedRoutes in custom/app/routes.go; entities also have RegisterRoutes. Choose the appropriate extension point without editing generated bootstrap.
The custom directory does not grant authorization automatically. Route dependencies expose RequireAuthenticated, RequireAdminOperation, RequireExternalUser and CurrentPrincipal. Apply the appropriate server-side guard. Signing in does not grant every operation, and custom record-selection rules must also run on the server.
Build the backend and check the changed use case. Custom routes do not automatically appear in generated OpenAPI; your team maintains their contract.
Continue with Admin extensions and The custom development workflow.