Skip to content

Overview

Laju Go is a single-entry Go application. One binary, one process, one main(). There is no separate API server — Inertia.js makes the Svelte 5 frontend a server-rendered SPA that talks to the same Fiber process that serves HTML.

Everything boots from cmd/laju-go/main.go. That file owns the entire dependency graph: it opens SQLite, runs Goose migrations, builds the querier, wires services into handlers, registers routes, and starts Fiber.

// cmd/laju-go/main.go — abbreviated
db, err := initDatabase(cfg.DBPath)
if err := runMigrations(db, "./migrations"); err != nil { /* ... */ }
querier := queries.NewQuerier(db)
sessionCache := cache.NewSessionCache()
sessionStore := session.New(querier, sessionCache, cfg.SessionTTL)
authService := services.NewAuthService(querier, services.AuthServiceConfig{ /* ... */ })
userService := services.NewUserService(querier)
inertiaService := services.NewInertiaService(assetService, sessionStore)
routeHandlers := routes.Handlers{
Public: handlers.NewPublicHandler(authService, userService, inertiaService, assetService),
Auth: handlers.NewAuthHandler(authService, sessionStore, inertiaService),
App: handlers.NewAppHandler(userService, sessionStore, inertiaService),
Upload: uploadHandler,
}
routes.SetupRoutes(app, routeHandlers, sessionStore, userService, mailerService, csrfMiddleware)

There is no service locator, no plugin registry, no reflection. Dependencies are constructed in main() and passed down by hand. If a handler needs something, it receives it in its constructor.

Browser (Svelte 5 + Inertia.js)
Fiber app ── global middleware (recover, compress, Inertia, CORS, CSRF)
routes/web.go ── route + per-group middleware (AuthRequired, Guest, rate limit)
app/handlers/ ── parse request, call service, return Inertia Render / Redirect / JSON
app/services/ ── business logic, auth flows, calls s.querier.* or s.cache.*
app/queries/ ── THE ONLY layer that executes SQL (generated by sqlc)
SQLite (CGO, mattn/go-sqlite3)

A GET that renders a page ends at h.inertiaService.Render(c, "app/Dashboard", fiber.Map{...}) — Inertia returns JSON for XHR visits and full HTML for the first load. A POST/PUT that mutates state ends at h.inertiaService.Redirect(c, "/path"), a 303 See Other that Inertia turns into a follow-up GET.

Layer Path Responsibility
Routes routes/web.go URL → handler mapping, middleware grouping. No logic.
Handlers app/handlers/ Parse request (BodyParser, params, session), call one service, shape the response. No business logic, no SQL.
Services app/services/ Business logic, auth flows, external APIs. Reads via s.querier.* or s.cache.*. Never opens a DB connection.
Queries app/queries/ Type-safe SQL, generated by sqlc from queries/*.sql. The only layer that touches the database. Never edited by hand.
Models app/models/ Pure data structs — domain entities and DTOs. No logic, no DB access, must not import app/queries.
Cache app/cache/ In-memory session cache (sync.RWMutex + map). Called via the Service layer, never from a Handler.
Session app/session/ DB-backed sessions with an in-memory cache front. store.Get(c), store.Flash(c, ...), store.CreateAuthenticatedSession(...).
Middlewares app/middlewares/ AuthRequired, Guest, AdminRequired, CSRFMiddleware, rate limiters.
Frontend frontend/src/ Svelte 5 + Inertia.js. Built by Vite into dist/, consumed by Go via dist/.vite/manifest.json.

The Go binary reads Vite’s asset manifest, so the frontend must build first:

vite build → dist/.vite/manifest.json → go build

npm run build:all (or make build) runs them in the right order. Reversing it produces a binary that references stale or missing assets.

  • One deploy artifact. A single Go binary plus dist/ and migrations/. No Node runtime in production.
  • Type safety end to end. sqlc generates Go from SQL; templ generates Go from templates; Svelte 5 is typed. The only untyped boundary is fiber.Map props passed to Inertia.
  • No hidden layers. Every dependency is visible in a constructor. Every SQL statement lives in queries/*.sql. Every route is registered in routes/web.go.

The single rule that holds this together — Handler → Service → Query, no skipping — is covered in Three-tier rule.