Project Structure

How to organize a GOaT project for clarity and scalability.

A GOaT project follows Go conventions with clear separation between handlers, business logic, and templates.

Standard Layout

myapp/
├── cmd/server/main.go        # Entry point, wiring
├── internal/
│   ├── handler/              # HTTP handlers (one file per domain)
│   │   ├── game.go
│   │   ├── game_api.go       # JSON API endpoints (for Alpine.js)
│   │   ├── auth.go
│   │   └── template.go       # Page rendering handlers
│   ├── service/              # Business logic, DB queries
│   │   ├── game.go
│   │   ├── user.go
│   │   └── sse.go            # SSE hub
│   ├── middleware/            # Auth, logging, CORS
│   │   └── auth.go
│   ├── model/                # sqlc generated code
│   │   ├── db.go
│   │   ├── models.go
│   │   └── queries.sql.go
│   └── routes.go             # All route registration
├── templates/
│   ├── layouts/layout.html   # Base HTML with head, body, nav
│   ├── pages/                # One file per page
│   │   ├── home.html
│   │   ├── lobby.html        # HTMX-driven page
│   │   └── game.html         # Alpine.js reactive page
│   └── partials/             # Reusable HTML fragments
│       ├── nav.html
│       └── scoreboard.html
├── styles/main.css           # Tailwind + DaisyUI entry
├── static/                   # Compiled CSS, images, fonts
├── model/
│   ├── migrations/           # Numbered SQL migrations
│   ├── queries.sql           # sqlc source queries
│   └── sqlc.yaml
├── e2e/                      # Playwright E2E tests
│   ├── tests/
│   └── playwright.config.ts
└── Dockerfile

Key Principles

Handlers are thin. They parse requests, call services, and render responses. No business logic.

func (h *GameHandler) CreateGame(w http.ResponseWriter, r *http.Request) {
    game, err := h.gameSvc.CreateGame(r.Context())
    if err != nil {
        http.Error(w, "Failed to create game", 500)
        return
    }
    http.Redirect(w, r, "/game/"+strconv.Itoa(int(game.ID)), http.StatusSeeOther)
}

Services own the logic. Database queries, validation, state transitions — all in the service layer.

Templates are organized by scope. Layouts wrap everything, pages define content blocks, partials are reusable fragments.

Routes are centralized. One file (routes.go) registers every route. You can see your entire API surface at a glance.

When to Split Files

  • One handler file per domain (game, auth, user, tournament)
  • Separate _api.go files when you have both HTMX and JSON endpoints for the same domain
  • One service file per domain
  • Keep middleware files small and focused