App
Application Structure
Learn the conventional directories and ownership boundaries of a GoLazy application.
The application tree
A GoLazy application keeps its executable small, places web presentation
behavior under app, and keeps business services at the top level:
.mise/
tasks/
app/
controllers/
base_controller.go
home_controller/
mailers/
public/
assets/
javascript/
styles/
views/
cmd/app/
init/
seo.go
js.toml
package.json
mise.toml
.secrets/
services/
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. Shared controller
behavior lives in app/controllers/base_controller.go. Each concrete
controller lives in its own package directory under app/controllers, embeds
the shared base controller, resolves its services in its constructor, and
exposes action methods. The shared base inherits framework error handling, so
apps can rely on the framework default error page and add
app/views/app/error.html.tpl only when they need an app-specific override.
Controller constructors receive only context.Context:
func New(ctx context.Context) (*HomeController, error)
Controllers are request-local. Never cache a concrete controller or share it between requests because render data and response state are mutable.
app/mailers
Mailers render email bodies with the same view system as controllers. Keep
mailer types under app/mailers and place their templates in matching view
directories:
app/mailers/poll_mailer.go
app/views/layouts/mailer.text.tpl
app/views/layouts/mailer.html.tpl
app/views/poll_mailer/admin_link.text.tpl
app/views/poll_mailer/admin_link.html.tpl
Mail delivery is configured as a service in init/dependencies.go. The framework
provides golazy.dev/lazymailer for standard-library MIME message building,
SMTP delivery, memory delivery, and custom delivery implementations.
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/dependencies.go initializes shared application dependencies once and
places them into a context.
init/seo.go is optional. Public apps that need application-wide SEO defaults
define func SEO(context.Context) []lazyseo.Option there and wire it through
lazyapp.Config.SEO.
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.
services
Services contain core application behavior that does not belong to HTTP
handling or template rendering. They live outside app because the web layer is
only one presentation of the business behavior. 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 keeps browser JavaScript source outside public files and generated browser JavaScript under public files:
app/js/app.js
app/js/controllers/hello_controller.js
app/public/assets/importmap.json
app/public/assets/lazyshaft/
app/js is app-owned source. The assets/lazyshaft files and importmap are
generated by lazy js from js.toml and app/js.
app/styles
Stylesheet source files that need a build step live outside public files. The default Tailwind input is:
app/styles/application.css
lazy tailwind compiles that source into:
app/public/styles.css
Keep templates linked to /styles.css through stylesheet; the output remains
a normal public asset and receives the same fingerprinted permalink, ETag, and
cache policy as any other public file.
Read Stylesheets for the full Tailwind workflow.
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 lazy js for the full manifest shape and verification workflow.
mise.toml and secrets
The sample app includes a mise.toml development toolchain file. It installs
Node.js and the age, sops, and usage command-line tools. Ordinary
checked-in development values live directly in mise.toml; secret-shaped
checked-in examples live in .secrets/development.env, which mise.toml loads
for commands run through mise. It does not install Go because Go already
bundles multi-version support through the module go directive and toolchain
selection. Helper tools are pinned to explicit versions for reproducible clean
installs. Mise requires a one-time trust approval before reading this config
because it loads an environment file; lazy new performs that trust step for
generated apps.
The checked-in .secrets/development.env file is only for secret-shaped
development examples. Production deployments are responsible for supplying real
secrets through their platform or secret store.
Standalone mise task scripts live under .mise/tasks. The sample app includes
.mise/tasks/hello.go, which mise discovers as hello and runs with:
mise run hello
This task is an educational example that teaches how to run a Go script through
mise. Tests do not need a task wrapper; run go test ./... directly.
cmd/app
The executable initializes the application and starts it with
lazyapp.App.ListenAndServe. The framework uses ADDR, then PORT, then
127.0.0.1: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.
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/lazycontrollerowns controller rendering and typed HTTP errors.golazy.dev/lazyerrorsowns caller-prefixed application errors with typed backtrace frames.golazy.dev/lazyroutesowns route scopes, route metadata, controller action routing, and REST resources.golazy.dev/lazydispatchowns middleware dispatch, route-only response buffering, dynamic route ETags, and request flow.golazy.dev/lazyassetsowns public and generated asset registration, fingerprints, integrity values, ETags, cache policy, and unpacking.golazy.dev/lazysseowns Server-Sent Events formatting and flushing.golazy.dev/lazyappwires the application context, views, routes, dispatcher, and public files into one handler.golazy.dev/lazyviewowns rendering, helpers, and template engines.
Application packages must not be imported by the framework.