Server Architecture Overview
Learn the architectural principles and patterns behind Tracks-generated applications.
What Tracks Generates
When you run tracks new myapp, you get a production-ready Go web application with:
- Clean layered architecture - Clear separation between HTTP, domain logic, and data access
- Dependency injection - No global state, testable components
- Interface-based design - Flexible, mockable dependencies
- Type-safe everything - SQLC for SQL, templ for HTML, route constants for URLs
- Ready for growth - Add features incrementally without refactoring
This isn't a framework you import - it's code you own. Every file is readable, modifiable, and follows idiomatic Go patterns.
Core Principles
1. Dependency Injection
Services receive dependencies through constructors, not global variables:
// ✅ CORRECT: Dependencies injected
type Service struct {
repo UserRepository
}
func NewService(repo UserRepository) *Service {
return &Service{repo: repo}
}
// ❌ WRONG: Global state
var globalDB *sql.DB
type Service struct{}
func (s *Service) GetUser(id string) (*User, error) {
return globalDB.Query(...)
}
Why? Explicit dependencies make code testable and coupling visible.
2. Interface-Based Design
Accept interfaces, return structs:
// ✅ CORRECT: Accept interface
func NewHandler(userService interfaces.UserService) *Handler {
return &Handler{userService: userService}
}
// ❌ WRONG: Depend on concrete type
func NewHandler(userService *users.Service) *Handler {
return &Handler{userService: userService}
}
Why? Interfaces enable testing with mocks and decouple layers.
3. Context Propagation
Always pass context.Context as the first parameter:
// ✅ CORRECT
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
return s.repo.FindByID(ctx, id)
}
// ❌ WRONG: No context
func (s *Service) GetUser(id string) (*User, error) {
return s.repo.FindByID(id)
}
Why? Enables request cancellation, deadlines, and tracing.
4. Explicit Error Handling
Wrap errors with context using %w:
// ✅ CORRECT: Wrapped errors
if err != nil {
return nil, fmt.Errorf("getting user %s: %w", id, err)
}
// ❌ WRONG: Error chain broken
if err != nil {
return nil, errors.New("failed to get user")
}
Why? Preserves error chain for debugging while adding context.
5. Type Safety
- SQLC generates type-safe Go code from SQL queries
- templ compiles HTML templates to Go at build time
- Route constants replace magic strings with compile-time checks
No reflection, no runtime string parsing.
Request Flow
Here's the journey of an HTTP request through a Tracks application:
┌─────────────────┐
│ HTTP Request │
└────────┬────────┘
│
▼
┌─────────────────────────────────────┐
│ Middleware Chain │
│ ┌────────────────────────────────┐ │
│ │ 1. Request ID │ │
│ │ 2. Logging │ │
│ │ 3. Security (CORS, CSP, HSTS) │ │
│ │ 4. Authentication │ │
│ └────────────────────────────────┘ │
└────────┬────────────────────────────┘
│
▼
┌─────────────────┐
│ Router (Chi) │ Matches route pattern
└────────┬────────┘
│
▼
┌─────────────────────────────────────┐
│ Handler │
│ • Validates input (DTOs) │
│ • Calls service(s) via interfaces │
│ • Orchestrates cross-domain ops │
│ • Formats response (HTML via templ │
│ or JSON for APIs) │
└────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Service │
│ • Business logic │
│ • Domain validations │
│ • Calls repository via interface │
└────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Repository │
│ • Wraps SQLC-generated code │
│ • Database queries │
│ • Transaction management │
└────────┬────────────────────────────┘
│
▼
┌─────────────────┐
│ Database │ LibSQL / Postgres / SQLite
└─────────────────┘
Key Points:
- Middleware: Runs in order, can short-circuit the request
- Handler: Thin layer - orchestrates, doesn't contain business logic
- Service: Where business rules live
- Repository: Only layer that talks to the database
Layer Overview
Tracks applications are organized into four main layers:
HTTP Layer (internal/http/)
Handles web-facing concerns:
- server.go - HTTP server setup, graceful shutdown
- routes.go - Route registration, middleware chain
- routes/ - Domain-based route files (health.go, users.go, etc.) with type-safe constants
- handlers/ - HTTP request/response handling
- middleware/ - Composable middleware functions
- views/ - templ components for HTML rendering (HYPERMEDIA-first)
Responsibility: Convert HTTP into domain operations and back. Serves HTML by default via templ.
Interfaces Package (internal/interfaces/)
Defines contracts between layers:
- One file per domain (e.g.,
user.go,post.go) - Zero implementations - only interface definitions
- Consumed by handlers, implemented by services
Responsibility: Prevent import cycles, enable mocking.
Domain Layer (internal/domain/)
Contains business logic:
- service.go - Business rules, implements service interface
- repository.go - Data access, implements repository interface
- dto.go - Request/response data transfer objects
- Organized by domain (users/, posts/, health/)
Responsibility: Enforce business rules, coordinate data access.
Database Layer (internal/db/)
Manages data persistence:
- db.go - Connection setup, transaction helpers
- migrations/ - Goose SQL migrations
- queries/ - Hand-written SQL (SQLC input)
- generated/ - SQLC-generated Go code (don't edit!)
Responsibility: Type-safe database access.
Dependency Flow
Dependencies flow in one direction: inward.
┌──────────────────────────────────────────┐
│ HTTP Layer (handlers, middleware) │
│ depends on ↓ │
├──────────────────────────────────────────┤
│ Interfaces (service/repo contracts) │
│ implemented by ↓ │
├──────────────────────────────────────────┤
│ Domain Layer (services, repositories) │
│ depends on ↓ │
├──────────────────────────────────────────┤
│ Database Layer (queries, connections) │
└──────────────────────────────────────────┘
Rules:
- HTTP can depend on domain - Handlers use service interfaces
- Domain cannot depend on HTTP - Services don't know about HTTP
- Interfaces bridge the gap - Defined in consumer packages
This creates a clean separation where business logic has zero HTTP dependencies.
How This Differs from Typical Go Apps
vs. Fat Controllers
Typical: Controllers contain business logic
// ❌ Business logic in HTTP handler
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
user := parseRequest(r)
// Validation logic in handler
if user.Age < 18 {
http.Error(w, "too young", 400)
return
}
// Database access in handler
err := h.db.Insert(user)
// ...
}
Tracks: Handlers orchestrate, services contain logic
// ✅ Thin handler, delegates to service
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), 400)
return
}
user, err := h.userService.Create(r.Context(), req)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
json.NewEncoder(w).Encode(user)
}
vs. Interfaces with Implementations
Typical: Interfaces next to implementations
users/
├── service.go # Contains UserService interface
└── service_impl.go # Implements UserService
Tracks: Interfaces in consumer packages
interfaces/
└── user.go # UserService interface
domain/users/
└── service.go # Implements UserService
Why? Prevents import cycles, enables clean mocking.
vs. Manual SQL
Typical: Hand-written SQL strings
// ❌ Error-prone, not type-safe
row := db.QueryRow("SELECT id, name, email FROM users WHERE id = $1", id)
var user User
err := row.Scan(&user.ID, &user.Name, &user.Email)
Tracks: SQLC generates type-safe code
// ✅ Type-safe, compile-time checked
user, err := r.queries.GetUser(ctx, id)
SQL is in .sql files, Go code is generated from it.
Next Steps
- Layer Guide - Deep dive into each layer's responsibilities
- Routing Guide - HYPERMEDIA-first routing and domain-based organization
- Patterns - Common patterns for extending your app
- Testing - Testing strategies and examples
See Also
- CLI: tracks new - Creating projects
- CLI: Commands Reference - All CLI commands