Structuring Go Web Applications

Updated

September 8, 2026

Structuring Go Web Applications

Overview

A modular layout keeps web apps maintainable: HTTP stays at the edge, domain rules stay pure, and databases hide behind interfaces. This chapter shows a practical Bookstore layout—not the only valid layout, but one that scales from a tutorial to a small product.

Goals of structure

  1. Find things fast — routes, handlers, domain, persistence in predictable places
  2. Test in isolation — swap real DB for mocks without rewriting handlers
  3. Avoid circular imports — packages depend inward (domain) not sideways forever
  4. Ship one binarycmd/bookstore is the process entry; libraries live under internal/

Dependency direction

  cmd/bookstore
       │
       ▼
   httpapi  ──uses──►  store (interfaces)
       │                    ▲
       │                    │ implements
       ▼                    │
    domain  ◄────────  store/memory, store/postgres
  • domain knows nothing about HTTP or SQL.
  • store defines interfaces and implementations.
  • httpapi depends on interfaces + domain types.
  • main constructs concrete types and injects them.

Config from environment

package config

import (
    "os"
    "time"
)

type Config struct {
    Addr            string
    DatabaseURL     string
    SessionSecret   string
    ShutdownTimeout time.Duration
}

func Load() Config {
    return Config{
        Addr:            getenv("ADDR", ":8080"),
        DatabaseURL:     os.Getenv("DATABASE_URL"),
        SessionSecret:   getenv("SESSION_SECRET", "dev-only-change-me"),
        ShutdownTimeout: 10 * time.Second,
    }
}

func getenv(k, def string) string {
    if v := os.Getenv(k); v != "" {
        return v
    }
    return def
}

Never commit production secrets. Dev defaults are fine if loudly unsafe.

Domain types (Bookstore)

package domain

import "fmt"

type Book struct {
    ID     string `json:"id"`
    Title  string `json:"title"`
    Author string `json:"author"`
    ISBN   string `json:"isbn"`
    Price  int    `json:"price_cents"` // store money as integer cents
}

func (b Book) Validate() error {
    if b.Title == "" {
        return fmt.Errorf("title is required")
    }
    if b.Author == "" {
        return fmt.Errorf("author is required")
    }
    if b.Price < 0 {
        return fmt.Errorf("price_cents must be >= 0")
    }
    return nil
}

Validation on the domain type keeps handlers short and reuses the same rules for HTML forms and JSON APIs.

Repository interface

package store

import (
    "context"

    "example.com/bookstore/internal/domain"
)

type BookRepository interface {
    List(ctx context.Context) ([]domain.Book, error)
    Get(ctx context.Context, id string) (domain.Book, error)
    Create(ctx context.Context, b domain.Book) (domain.Book, error)
}

Handlers depend on BookRepository, not on Postgres. Chapter 275 implements memory and SQL; chapter 279 mocks this interface.

Wiring in main

package main

import (
    "log"
    "net/http"
    "time"

    "example.com/bookstore/internal/config"
    "example.com/bookstore/internal/httpapi"
    "example.com/bookstore/internal/store/memory"
)

func main() {
    cfg := config.Load()
    books := memory.NewBookRepo()
    api := httpapi.NewServer(books)

    srv := &http.Server{
        Addr:              cfg.Addr,
        Handler:           api.Handler(),
        ReadHeaderTimeout: 5 * time.Second,
    }
    log.Printf("bookstore listening on %s", cfg.Addr)
    log.Fatal(srv.ListenAndServe())
}

main is assembly, not business logic. When you add auth, sessions, and Postgres, only wiring and constructors grow here.

Package naming habits

Prefer Avoid
httpapi, store, domain utils, helpers, common dumping grounds
Short import paths under internal/ Giant models package everything imports
One concern per package Circular handler ↔︎ service ↔︎ handler

If two packages need each other constantly, merge them or extract a smaller shared types package.

Modular design without over-engineering

For a learning Bookstore you do not need:

  • Full clean-architecture ceremony with six layers
  • A DI framework
  • Microservices split by “books” vs “users”

You do need:

  • Interfaces at I/O boundaries
  • context.Context on store methods
  • Config outside code
  • A single place that registers routes

Graceful shutdown sketch

go func() {
    log.Fatal(srv.ListenAndServe())
}()

// on signal:
ctx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
defer cancel()
_ = srv.Shutdown(ctx)

Production processes should finish in-flight requests instead of hard-killing connections.

Rules of thumb

Do Don’t
Put app code under internal/ Export everything as a public library by accident
Inject repositories into handlers Reach for package-level global DB from every file
Keep main thin Grow a 2k-line main.go of handlers
Validate in domain or a small service Duplicate validation in every handler

Try next

  1. Scaffold the directories above and compile with an empty mux.
  2. Move healthz into httpapi and construct it from main.
  3. Add a UserRepository interface stub for the auth chapter.