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>
{{stylesheet "/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 stylesheet, 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 as a route prototype, then copied into pooled request instances.- Returning
nillets GoLazy renderhome/index.html.tpl. - The layout receives
.content. path_forcomes from the router helper registration.stylesheetcomes 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.