Start Here

Application Structure

Learn the conventional directories and ownership boundaries of a GoLazy application.

By Guillermo Alvarez - Published - Updated

The application tree

A GoLazy application keeps its executable small and places application behavior under app:

app/
  controllers/
  helpers/
  public/
    assets/
    javascript/
  services/
  views/
cmd/app/
init/
js.toml
lib/
test/

The structure is conventional, but every directory contains ordinary Go, standard-library templates, or static files.

app/controllers

Controllers translate HTTP requests into application work. A concrete controller embeds the application BaseController, resolves its services in New, and exposes action methods.

Controller constructors receive only context.Context:

func New(ctx context.Context) (*PostsController, error)

Controllers are request-local. Never cache a concrete controller or share it between requests because render data and response state are mutable.

app/helpers

Helpers contain template functions owned by the application. Keep them as plain Go functions and expose the template names through RegisterHelpers:

func RegisterHelpers() map[string]any

init/app.go passes that map to lazyapp.Config.Helpers, and templates call helpers by their registered names.

init

init/app.go is the application composition entry point. It calls lazyapp.New, passes embedded views and public files, provides the app context initializer, and points the framework at Draw.

init/context.go initializes shared application dependencies once and places them into a context.

init/routes.go is the routing table. Its public entry point is:

func Draw(router *lazyroutes.Scope)

Draw receives the framework-created route scope. It does not create or return the application handler, and it does not install dispatch middleware.

app/services

Services contain application behavior that does not belong to HTTP handling or template rendering. Each service defines typed WithContext and FromContext helpers so its dependency contract stays visible.

Services should be deterministic where practical and independently testable.

app/views

Views use Go's html/template syntax:

app/views/layouts/<layout>.html.tpl
app/views/<controller>/<action>.html.tpl

The default layout is layouts/app.html.tpl. Controller view names are derived from route metadata, so ordinary actions can set data and return nil instead of passing a view path manually.

app/public

Public files are embedded, registered with lazyassets, and served after application route lookup. For example:

app/public/styles.css

is available as:

/styles.css
/styles-<hash>.css

The logical path is useful for direct requests. Templates should call asset_path to link the content-hashed permanent path. Explicit application routes take precedence over public files.

The sample app also keeps browser JavaScript under public files:

app/public/javascript/application.js
app/public/assets/importmap.json
app/public/assets/lazyshaft/

application.js is app-owned code loaded directly by the browser. The assets/lazyshaft files and importmap are generated by lazy js from js.toml.

js.toml

js.toml declares JavaScript library entrypoints for lazy js. Keep it small:

[entrypoint.turbo]
module = "@hotwired/turbo"

[entrypoint.stimulus]
module = "@hotwired/stimulus"

Read JavaScript Libraries for the full manifest shape and verification workflow.

cmd/app

The executable initializes the application and starts it with lazyapp.App.ListenAndServe. The framework uses ADDR, then PORT, then :3000 as the listen address.

Keep business logic out of main. It should remain possible to construct the complete handler in tests without starting a network listener.

lib

Application-specific adapters can live in lib. The sample app keeps its Goldmark adapter in lib/markdown, leaving the framework independent of that third-party dependency.

test

Package tests remain next to their code. Full application integration tests live in test and construct the same application handler used by the executable.

Framework boundaries

Generic behavior belongs in the golazy.dev module:

  • golazy.dev/lazycontroller owns controller rendering and typed HTTP errors.
  • golazy.dev/lazyroutes owns route scopes, route metadata, controller action routing, and REST resources.
  • golazy.dev/lazydispatch owns middleware dispatch, route-only response buffering, dynamic route ETags, and request flow.
  • golazy.dev/lazyassets owns public and generated asset registration, fingerprints, integrity values, ETags, cache policy, and unpacking.
  • golazy.dev/lazyapp wires the application context, views, routes, dispatcher, and public files into one handler.
  • golazy.dev/lazyview owns rendering, helpers, and template engines.

Application packages must not be imported by the framework.