Controllers handle HTTP requests and return responses. Catuaba generates two types: HTML handlers (for scaffold) and JSON API controllers (for api).
Generated by catuaba g scaffold, these live in app/controllers/{resource}/ and render Templ views.
catuaba g scaffold Post title:string body:text published:booleanCreates 7 handler files in app/controllers/posts/:
| File | Route | Purpose |
|---|---|---|
index.go |
GET /posts |
List with pagination |
new.go |
GET /posts/new |
Render new form |
create.go |
POST /posts |
Process form, create record |
show.go |
GET /posts/:id |
Show detail view |
edit.go |
GET /posts/:id/edit |
Render edit form |
update.go |
POST /posts/:id |
Process form, update record |
delete.go |
POST /posts/:id/delete |
Delete record |
Each handler is a standalone function in its own file:
package posts
import (
"net/http"
"myapp/app/models"
views "myapp/app/views/posts"
"myapp/database"
"myapp/middleware"
"github.com/gin-gonic/gin"
)
// Show handles GET /posts/:id
func Show(ctx *gin.Context) {
var post models.Post
if err := database.DB.First(&post, ctx.Param("id")).Error; err != nil {
ctx.String(http.StatusNotFound, "Record not found")
return
}
flashMsg, flashType := middleware.GetFlash(ctx)
ctx.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
views.Show(post, flashMsg, flashType).Render(ctx.Request.Context(), ctx.Writer)
}Key patterns:
- Database: use
database.DB(GORM) - URL params:
ctx.Param("id") - Query params:
ctx.DefaultQuery("page", "1") - Form binding:
ctx.ShouldBind(¶ms)with aCreateParams/UpdateParamsstruct - Render view:
views.Show(data, ...).Render(ctx.Request.Context(), ctx.Writer) - Flash messages:
middleware.SetFlash(ctx, "success", "Post created!") - Redirect:
ctx.Redirect(http.StatusSeeOther, "/posts")
Create and Update handlers use typed param structs with form binding tags:
type CreateParams struct {
Title string `form:"title"`
Body string `form:"body"`
Published bool `form:"published"`
}The update handler assigns fields explicitly and uses database.DB.Save() (not .Updates()) to correctly save zero-values like false for booleans.
Generated by catuaba g api, these live in app/controllers/api/{resource}/ and return JSON.
catuaba g api Order total:float status:stringCreates 5 controller files + tests in app/controllers/api/orders/:
| File | Route | Purpose |
|---|---|---|
index.go |
GET /api/orders |
Paginated list (JSON) |
create.go |
POST /api/orders |
Create (JSON body) |
show.go |
GET /api/orders/:id |
Single record (JSON) |
update.go |
PUT/PATCH /api/orders/:id |
Update (JSON body) |
delete.go |
DELETE /api/orders/:id |
Delete |
package orders
import (
"net/http"
"myapp/app/models"
"myapp/database"
"github.com/gin-gonic/gin"
)
type CreateParams struct {
Total float64 `json:"total" binding:"required"`
Status string `json:"status" binding:"required"`
}
func Create(ctx *gin.Context) {
var params CreateParams
if err := ctx.ShouldBindJSON(¶ms); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
order := &models.Order{
Total: params.Total,
Status: params.Status,
}
if err := database.DB.Create(order).Error; err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusCreated, order)
}Key differences from HTML handlers:
- Uses
ctx.ShouldBindJSON()(notShouldBind) - JSON binding tags:
`json:"total" binding:"required"` - Boolean fields skip
binding:"required"(sincefalseis zero-value) - Returns
ctx.JSON()instead of rendering views - API routes are under
/api/prefix (CSRF middleware is skipped)
Each API controller comes with a test file:
func TestCreate(t *testing.T) {
// Sets up test router, sends POST request, asserts 201 status
}For custom pages that don't follow CRUD patterns:
catuaba g controller Auth login logout registerCreates app/controllers/auth.go with stub handlers for each method.
All routes are auto-injected into config/routes.go:
func SetupRoutes(routes *gin.Engine) {
routes.GET("/", controllers.Home)
// Post routes (scaffold)
routes.GET("/posts", posts.Index)
routes.GET("/posts/new", posts.New)
routes.POST("/posts", posts.Create)
routes.GET("/posts/:id", posts.Show)
routes.GET("/posts/:id/edit", posts.Edit)
routes.POST("/posts/:id", posts.Update)
routes.POST("/posts/:id/delete", posts.Delete)
// Order API routes
routes.GET("/api/orders", api_orders.Index)
routes.POST("/api/orders", api_orders.Create)
// ...
// [catuaba:routes] — generators inject new routes above this line
}List all routes: catuaba routes