Start Here

Single-File App

Build a complete GoLazy application in one Go file before splitting it into the conventional structure.

By Guillermo Alvarez - Published - Updated

Start with one file

GoLazy encourages a project structure that scales well, but it does not require that structure. A GoLazy app is still a Go program. You can start with one Go file, keep the application code visible, and still keep layouts, views, and public assets as normal files.

Create a module:

mkdir hello-golazy
cd hello-golazy
go mod init example.com/hello-golazy
go get golazy.dev@latest

Create this structure:

hello-golazy/
  main.go
  public/
    styles.css
  views/
    home/
      index.html.tpl
    layouts/
      app.html.tpl

Create main.go, the only Go file:

package main

import (
    "context"
    "embed"
    "io/fs"
    "log"
    "net/http"

    "golazy.dev/lazyapp"
    "golazy.dev/lazycontroller"
    "golazy.dev/lazyroutes"
    _ "golazy.dev/lazyview/gotmpl"
)

//go:embed public views
var files embed.FS

type HomeController struct {
    lazycontroller.Base
}

func NewHomeController(ctx context.Context) (*HomeController, error) {
    base, err := lazycontroller.NewBase(ctx)
    if err != nil {
        return nil, err
    }
    return &HomeController{Base: base}, nil
}

func (c *HomeController) Index(_ http.ResponseWriter, _ *http.Request) error {
    c.Set("title", "Hello, GoLazy")
    c.Set("message", "A complete GoLazy app can start with one Go file.")
    return c.Render("index")
}

func Draw(router *lazyroutes.Scope) {
    router.Get("/", NewHomeController, (*HomeController).Index)
}

func Views() (fs.FS, error) {
    return fs.Sub(files, "views")
}

func Public() (fs.FS, error) {
    return fs.Sub(files, "public")
}

func main() {
    app := lazyapp.New(lazyapp.Config{
        Name:   "hello_golazy",
        Drawer: Draw,
        Views:  Views,
        Public: Public,
    })

    log.Println("listening on http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", app))
}

Create views/layouts/app.html.tpl:

<!doctype html>
<html lang="en">
  <head>
    <title>{{.title}}</title>
    <link rel="stylesheet" href="/styles.css">
  </head>
  <body>
    <nav><a href="{{path_for "root"}}">Home</a></nav>
    <main>{{.content}}</main>
  </body>
</html>

Create views/home/index.html.tpl:

<h1>{{.title}}</h1>
<p>{{.message}}</p>

Create public/styles.css:

:root {
  font-family: system-ui, sans-serif;
}

body {
  margin: 2rem auto;
  max-width: 42rem;
}

Run it:

go run .

Open http://localhost:8080.

What is already here

This one Go file has the same core pieces as a larger GoLazy app:

  • lazyapp.New assembles the application.
  • Draw registers the route table.
  • HomeController is constructed once per request.
  • Render("index") resolves home/index.html.tpl.
  • The layout receives .content.
  • path_for comes from the router helper registration.
  • Public serves files from public.

Nothing special happened outside Go. There is no generator and no hidden runtime. main.go is a normal main package, and the templates and stylesheet are normal files embedded into the binary.

Add one more route

The shape stays the same as you add behavior:

func (c *HomeController) About(_ http.ResponseWriter, _ *http.Request) error {
    c.Set("title", "About")
    c.Set("message", "The same controller can render another action.")
    return c.Render("about")
}

func Draw(router *lazyroutes.Scope) {
    router.Get("/", NewHomeController, (*HomeController).Index)
    router.Get("/about", NewHomeController, (*HomeController).About)
}

You would also add views/home/about.html.tpl.

Why apps grow into directories

The single-file version is useful because it shows the real moving parts. As the app grows, the same parts start wanting names of their own:

  • Views move from views into app/views.
  • Controllers become easier to scan under app/controllers.
  • Shared application work moves into app/services.
  • Public files move from public into app/public.
  • Startup and routes settle into top-level init.
  • Integration tests get a stable home under test.

That structure is a convention, not a cage. It gives people and coding agents predictable places to look, while Go still lets a small app stay small.

Continue with Application Structure when the one-file version starts to feel crowded.