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/
helpers/
mailers/
public/
assets/
javascript/
styles/
views/
cmd/app/
init/
js.toml
package.json
lib/
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. 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.
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/context.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/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.
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, and loads
.secrets/development.env 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. 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 development
examples. Applications should read ordinary environment variables, such as
SECURE_COOKIE_KEY, and production deployments are responsible for supplying
real values 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 keeps small project scripts close to the app while avoiding extra
mise.toml entries.
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/lazycontrollerowns controller rendering and typed HTTP errors.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.