Core Framework

Controllers

Use pooled request-local controllers to coordinate services, prepare view data, and return HTTP errors.

By Guillermo Alvarez - Published - Updated

Controller shape

A concrete controller embeds the application's base controller and declares only the services it needs:

type PostsController struct {
    controllers.BaseController
    posts *postservice.Service
}

The constructor receives an application context:

func New(ctx context.Context) (*PostsController, error) {
    base, err := controllers.NewBaseController(ctx)
    if err != nil {
        return nil, err
    }

    posts, ok := postservice.FromContext(ctx)
    if !ok {
        return nil, fmt.Errorf(
            "posts service is missing from application context",
        )
    }

    return &PostsController{
        BaseController: base,
        posts: posts,
    }, nil
}

Do not add renderer, service, request, writer, or view-path parameters to concrete constructors. Those dependencies are resolved from context or fixed by the controller.

Request lifecycle

Routes use scope methods:

router.Get(
    "/posts",
    posts.New,
    (*posts.PostsController).Index,
)

When routes are drawn, GoLazy calls the controller constructor once and stores the result as a prototype. For each request, the route action wrapper:

  1. Borrows a controller instance from the route's pool.
  2. Copies the route prototype into that instance.
  3. Binds the request, response writer, and route metadata to the controller.
  4. Runs BeforeAction when the controller implements it.
  5. Runs the selected action.
  6. Renders the matching view when the action returns without writing a response.
  7. Converts any returned error into an HTTP response.
  8. Resets request state and returns the instance to the pool.

This lifecycle prevents mutable render data from leaking between requests. lazycontroller stays focused on controller state, rendering, and typed HTTP errors. Route definitions live in lazyroutes; request dispatch lives in lazydispatch.

Use BeforeAction for request-time setup that should run before every action:

func (c *BaseController) BeforeAction() error {
    c.Set("currentTime", c.timeService.Now().Format("2006-01-02 15:04:05 MST"))
    return nil
}

Actions

Actions use one signature:

func (c *Controller) Action(w http.ResponseWriter, r *http.Request) error

An action can use w and r directly, but ordinary HTML actions normally set data and return nil:

func (c *HomeController) Index(_ http.ResponseWriter, _ *http.Request) error {
    c.Set("title", "Home")
    return nil
}

GoLazy renders the default controller/action view automatically when the action has not already written or rendered a response. Call Render explicitly only when an action needs to render a different view.

View data

Set adds a value to the render data:

c.Set("posts", c.posts.List())

The value is available to both the controller view and its layout. Template data is escaped by default.

Layouts

Controllers use the app layout by default. Select another embedded layout before returning:

c.SetLayout("admin")
return nil

This resolves layouts/admin.html.tpl.

HTTP errors

Return lazycontroller.Error when an expected failure needs a specific status:

return lazycontroller.Error(
    http.StatusNotFound,
    fmt.Errorf("post %q not found", slug),
)

Unexpected errors become 500 Internal Server Error. The response contains the standard status text, while the wrapped error remains available to callers and future logging infrastructure.

Controller design

Keep actions short:

  • Read route values and request input.
  • Call services.
  • Set view data.
  • Return nil for the default view, render explicitly when needed, or return an error.

Move reusable application work into services rather than growing controller methods into business-logic containers.