Controllers
Controllers are the HTTP boundary. They receive Echo contexts, parse and validate input, coordinate injected models or services, and choose an Inertia, Templ, JSON, redirect, or error response.
Explicit dependencies
Model APIs are values such as models.Products, not package globals. Fx supplies them (and the Inertia renderer when needed) to the controller constructor:
1type Products struct {
2 products models.Products
3 renderer *inertia.Renderer
4}
5
6func NewProducts(products models.Products, renderer *inertia.Renderer) Products {
7 return Products{products: products, renderer: renderer}
8}
Use etx.Request().Context() when calling models and services so cancellation crosses the HTTP boundary. Do not stash collaborators on Echo context keys.
Inertia (default UI)
Generated Inertia apps inject *inertia.Renderer. One Page call covers the first HTML document and later JSON visits:
1func (p Products) Index(etx *echo.Context) error {
2 products, err := p.products.All(etx.Request().Context())
3 if err != nil {
4 return err
5 }
6
7 return p.renderer.Page(
8 etx,
9 "Products/Index",
10 inertia.FromStruct(ProductIndexProps{Items: toProductData(products)}),
11 ).Render()
12}
Validation failures stay on the same visit with protected errors:
1if validationErrors, ok := validation.As(err); ok {
2 return p.renderer.Page(etx, "Products/Edit", props).
3 ValidationErrors(validationErrors.ToMap()).
4 Render()
5}
Redirect helpers:
| Method | Use |
|---|---|
Redirect(etx, location, status) |
Ordinary Inertia-aware redirect (POST upgrades 302 to 303) |
Location(etx, location) |
Hard visit / full document load (login success often uses this) |
1return p.renderer.Redirect(etx, routes.ProductIndex.URL(), http.StatusSeeOther)
Map domain rows to payload structs before Page. Do not pass models.Product or narsilc types into props. See Props and Shared Data and Redirects.
Templ and JSON
Templ controllers skip the renderer and call hypermedia helpers:
1func (p Products) Index(etx *echo.Context) error {
2 products, err := p.products.All(etx.Request().Context())
3 if err != nil {
4 return err
5 }
6 return hypermedia.RenderPage(etx, views.ProductsIndex(products))
7}
API / --api controllers return Echo JSON and keep application DTOs at the boundary:
1return etx.JSON(http.StatusOK, ProductIndexProps{Items: items})
| Response | Typical call | Contract |
|---|---|---|
| Inertia | renderer.Page(...).Render() |
Page name + JSON props / redirects |
| Templ | hypermedia.RenderPage / fragments |
Typed Templ components |
| JSON | etx.JSON |
Application DTOs only |
Generate controllers
1andurel generate controller Product
2andurel generate controller Product index show
3andurel generate controller Dashboard overview
4andurel generate controller v1/User --api
Use --model-name when a controller is backed by a differently named model, or --api for JSON handlers. Otherwise generation follows the UI recorded in andurel.toml (Inertia pages by default, or Templ).