Start Here

Getting Started

Run the sample application and follow a request from app startup to an HTML response.

By Guillermo Alvarez - Published - Updated

Prerequisites

GoLazy currently targets Go 1.26 or later. Confirm your installation:

go version

Begin with the sample_app repository, or start smaller with the single-file app.

Install the CLI and create an app

Install the GoLazy command:

go install github.com/golazy/lazy@latest

Create a new application from the matching sample_app tag:

lazy new github.com/guillermo/my_app

The command clones the sample app template matching the CLI version, removes its Git history, renames sample_app references to your module path, runs go mod tidy, and verifies the generated application with go test ./....

The lazy command

lazy is a helper command, not the framework runtime. It exists to make common development tasks shorter, such as creating a new application from the sample template and running the application command from the module root.

You can use the full framework without using lazy. A GoLazy application is a normal Go module with a normal cmd/app executable, and the framework is used through ordinary Go imports such as golazy.dev/lazyapp, golazy.dev/lazyroutes, and golazy.dev/lazycontroller.

The Go toolchain remains the source of truth:

go run ./cmd/app
go test ./...
go build ./cmd/app

The helper can also inspect the framework route table:

lazy routes

That command runs the application with the lazydev,printroutes build tags, prints the routes, and exits before the HTTP server starts.

When the application uses the sample JavaScript manifest, run the JavaScript library pipeline after changing js.toml, package.json, or library versions:

lazy js

That command bundles manifest entrypoints into embedded public assets and writes the importmap used by app-owned browser modules.

When the application uses Tailwind, run the stylesheet pipeline after changing Tailwind input, package files, or CSS dependencies:

lazy tailwind

Use watch mode while actively editing UI:

lazy tailwind --watch

Run the application

From the sample application directory, use the development runner:

lazy

lazy builds the application into a temporary binary, runs it with the lazydev build tag, serves the public address through a local development proxy, watches application files, restarts the app after successful rebuilds, and reloads browser pages that received HTML responses.

The server listens on :3000 by default. Open http://localhost:3000 in a browser.

Use ADDR or PORT to select another port or address:

PORT=4000 lazy
ADDR=127.0.0.1:4000 lazy

Run the application directly when you want the plain production-style process without the development proxy or watcher:

go run ./cmd/app

Follow the startup path

The executable asks the application initializer for the complete app and starts it:

if err := appinit.App().ListenAndServe(); err != nil {
    log.Fatal(err)
}

init/app.go wires the framework pieces:

func App() *lazyapp.App {
    return lazyapp.New(lazyapp.Config{
        Name:    "sample_app",
        Drawer:  Draw,
        Public:  app.Public,
        Views:   app.Views,
        Context: Context,
    })
}

This order matters:

  1. lazyapp.New opens views and initializes the renderer.
  2. Context initializes application services.
  3. Public files and generated asset sources are registered with lazyassets.
  4. Draw registers application routes on a lazyroutes.Scope.
  5. lazydispatch installs route-only response buffering, dynamic ETags, the router, and the asset fallback.
  6. ListenAndServe serves the completed application handler.

Follow one request

For GET /posts, the resource route binds the posts controller constructor to its Index action:

func Draw(router *lazyroutes.Scope) {
    router.Resources(posts.New)
}

Resources derives /posts from PostsController, registers the implemented REST actions, and creates a new controller for each request. The action loads data into the controller and returns:

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

Because the action has not written a response, GoLazy renders the default view:

app/views/posts/index.html.tpl

and composes it with:

app/views/layouts/app.html.tpl

Eligible route responses are buffered until the action returns and can receive a dynamic ETag. Public assets use their own asset ETags and cache policy.

Make a first change

Edit app/views/home/index.html.tpl while lazy is running. The command rebuilds the app, restarts it, and reloads browser pages that received HTML responses. Templates are still embedded at build time; development reload works by rebuilding the temporary binary.

If you run go run ./cmd/app directly, restart the Go process after changing Go code, templates, embedded content, or public files. If Tailwind is running in watch mode, it updates app/public/styles.css; lazy sees that public file change and reloads the app, while direct go run processes need a manual restart.

Build for production

GoLazy applications build with normal Go commands. There is no framework build step, code generation pass, or special packager:

go build ./cmd/app

That command uses Go's default output name. In the sample application the default name is app, which would collide with the existing app/ directory, so production builds normally choose an explicit artifact path with Go's standard -o flag:

go build -o bin/my_app ./cmd/app

The resulting binary is self-contained. Views, layouts, public files, and other embedded resources are compiled into the executable, so the deployed process does not need template files or a public directory beside it.

A minimal Dockerfile can copy only the binary into the runtime image:

FROM golang:1.26 AS build

WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app

FROM scratch
COPY --from=build /out/app /app
EXPOSE 3000
ENV PORT=3000
ENTRYPOINT ["/app"]

Verify the application

Run the complete test suite:

go test ./...

Before a release, also run:

go test -race ./...
go vet ./...
go build -o /tmp/sample-app ./cmd/app

Continue with Application Structure for a map of the repository.