Start Here
Single-File App
Build a complete GoLazy application in one Go file before splitting it into the conventional structure.
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.Newassembles the application.Drawregisters the route table.HomeControlleris constructed once per request.Render("index")resolveshome/index.html.tpl.- The layout receives
.content. path_forcomes from the router helper registration.Publicserves files frompublic.
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
viewsintoapp/views. - Controllers become easier to scan under
app/controllers. - Shared application work moves into
app/services. - Public files move from
publicintoapp/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.