Controllers

Controller Architecture

Use request-local controllers with uniform constructors and automatic rendering.

By Guillermo Alvarez - Published Updated

Shape a controller

A concrete controller embeds the application base controller and keeps only the services it needs:

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

The constructor receives the 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 missing")
    }

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

Do not add renderer, response writer, request, service, or view-path parameters to concrete controller constructors.

Keep controllers request-local

Routes point at a constructor, not a shared controller instance:

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

For each request, GoLazy constructs a fresh controller, runs the selected action, and then renders if the action did not already write a response. Mutable state such as Set, headers, layout selection, and status code stays on that one request.

Write standard actions

The standard action signature is:

func (c *PostsController) Index(
    w http.ResponseWriter,
    r *http.Request,
) error

Most HTML actions can ignore w, set template data, and return nil:

func (c *PostsController) Index(_ http.ResponseWriter, _ *http.Request) error {
    c.Set("title", "Posts")
    c.Set("posts", c.posts.List())
    return nil
}

GoLazy renders the default controller/action view after the action returns. Use Render("other") only when an action should render a different view.

Return expected errors

Use lazycontroller.Error for expected HTTP failures:

post, ok := c.posts.Get(r.PathValue("post_id"))
if !ok {
    return lazycontroller.Error(
        http.StatusNotFound,
        fmt.Errorf("post not found"),
    )
}

c.Set("post", post)
return nil

Unexpected errors become 500 Internal Server Error. The wrapped error is kept for callers and future logging, while the user receives the status response.

Set response metadata

Response helpers record metadata without committing the response early:

c.Status(http.StatusCreated)
c.Header().Set("Cache-Control", "no-store")
c.Set("post", post)
return nil

Automatic rendering can still run because Status does not call WriteHeader. If an action writes the response body manually, use the standard http.ResponseWriter methods directly.

Use Generators for typed action arguments and MIME and Formats for multi-format actions.