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"
"log"
"net/http"
"golazy.dev/lazyapp"
"golazy.dev/lazycontroller"
"golazy.dev/lazyroutes"
_ "golazy.dev/lazyview/gotmpl"
)
type HomeController struct {
lazycontroller.Base
}
func New(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 nil
}
//go:embed public views
var files embed.FS
func main() {
app := lazyapp.New(lazyapp.Config{
Name: "hello_golazy",
Drawer: func(router *lazyroutes.Scope) {
router.Get("/", New, (*HomeController).Index)
},
Public: lazyapp.MustSub(files, "public"),
Views: lazyapp.MustSub(files, "views"),
})
if err := app.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
Create views/layouts/app.html.tpl:
<!doctype html>
<html lang="en">
<head>
<title>{{.title}}</title>
<link rel="stylesheet" href="{{asset_path "/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:3000.
The action returns nil because GoLazy can render the matching
home/index.html.tpl view automatically when an action has not already written
a response. The layout links the stylesheet through asset_path, so the
browser receives the content-hashed permanent URL for the embedded public file.
What is already here
This one Go file has the same core pieces as a larger GoLazy app:
lazyapp.Newassembles the application.- The inline
Drawerregisters the route table. HomeControlleris constructed once per request.- Returning
nillets GoLazy renderhome/index.html.tpl. - The layout receives
.content. path_forcomes from the router helper registration.asset_pathcomes from the asset helper registration.lazyapp.MustSub(files, "public")registers embedded files frompublicas fingerprinted assets.
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 nil
}
Drawer: func(router *lazyroutes.Scope) {
router.Get("/", New, (*HomeController).Index)
router.Get("/about", New, (*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.