Connect a Database
Connect a Database
Section titled “Connect a Database”This guide shows how to wire a database connection into Plumego handlers using explicit constructor injection — the same pattern the reference service uses for all dependencies.
For the boundary rationale, see the Store Primer.
What this guide covers
Section titled “What this guide covers”- Holding a DB handle on a handler struct
- Wiring it through the app constructor
- Registering routes that use the injected handle
- Keeping the stable
storeprimitives out of business logic
Step 1 — Define a handler struct that holds the dependency
Section titled “Step 1 — Define a handler struct that holds the dependency”package handler
import ( "database/sql" "net/http"
"github.com/spcent/plumego/contract")
type ItemHandler struct { DB *sql.DB}
type itemResponse struct { ID int `json:"id"` Name string `json:"name"`}
func (h *ItemHandler) Get(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("id") if id == "" { _ = contract.WriteError(w, r, contract.NewErrorBuilder(). Type(contract.TypeRequired). Detail("field", "id"). Message("id is required"). Build()) return }
var item itemResponse err := h.DB.QueryRowContext(r.Context(), "SELECT id, name FROM items WHERE id = $1", id, ).Scan(&item.ID, &item.Name) if err == sql.ErrNoRows { _ = contract.WriteError(w, r, contract.NewErrorBuilder(). Type(contract.TypeNotFound). Message("item not found"). Build()) return } if err != nil { _ = contract.WriteError(w, r, contract.NewErrorBuilder(). Type(contract.TypeInternal). Message("database error"). Build()) return }
_ = contract.WriteResponse(w, r, http.StatusOK, item, nil)}Step 2 — Open the connection in the app constructor
Section titled “Step 2 — Open the connection in the app constructor”import ( "database/sql" _ "github.com/lib/pq" // or your driver)
type App struct { Core *core.App Cfg config.Config DB *sql.DB}
func New(cfg config.Config) (*App, error) { db, err := sql.Open("postgres", cfg.DatabaseDSN) if err != nil { return nil, fmt.Errorf("open db: %w", err) } if err := db.Ping(); err != nil { return nil, fmt.Errorf("ping db: %w", err) }
app := core.New(cfg.Core, core.AppDependencies{ Logger: plumelog.NewLogger(), }) recoveryMw, err := recovery.Middleware(recovery.Config{Logger: app.Logger()}) if err != nil { return nil, err } accesslogMw, err := accesslog.Middleware(accesslog.Config{Logger: app.Logger()}) if err != nil { return nil, err } app.Use(requestid.Middleware()) app.Use(recoveryMw) app.Use(accesslogMw)
return &App{Core: app, Cfg: cfg, DB: db}, nil}Step 3 — Inject into handlers at route registration
Section titled “Step 3 — Inject into handlers at route registration”func (a *App) RegisterRoutes() error { items := &handler.ItemHandler{DB: a.DB}
return a.Core.Get("/api/items", http.HandlerFunc(items.Get))}Step 4 — Close the connection on shutdown
Section titled “Step 4 — Close the connection on shutdown”The Start method in the reference service calls Shutdown via defer. Close the DB there:
func (a *App) Start() error { defer a.DB.Close() // ... Prepare, Server, ListenAndServe as normal}What this pattern gives you
Section titled “What this pattern gives you”- The handler struct is the boundary: its fields are the only dependencies it has.
- No package-global
DBvariable means tests can pass any*sql.DB— including an in-memory SQLite DB for unit tests. - The connection is opened once at startup and closed once at shutdown — no per-request connection logic.
If this does not work
Section titled “If this does not work”| Symptom | Check first |
|---|---|
| Handler sees a nil DB | Confirm the app constructor opens the connection and route registration passes DB: a.DB |
| Requests hang or ignore cancellation | Use QueryRowContext, QueryContext, or ExecContext with r.Context() |
| Tests are hard to isolate | Keep the DB behind handler fields or interfaces; avoid package-level globals |
| Shutdown leaks connections | Close the DB once from the app lifecycle, not per request |
| Domain logic imports storage primitives directly | Keep store and database/sql wiring at the handler/repository boundary |
Complete example in the reference app
Section titled “Complete example in the reference app”The reference service demonstrates the full dependency-injection pattern for database connections:
reference/standard-service/internal/app/app.go— constructor opens the DB and injects it into handler structsreference/standard-service/internal/app/routes.go— route wiring that passes populated handler structsreference/standard-service/main.go— bootstrap order: open → inject → run → defer close
Run it locally:
git clone https://github.com/spcent/plumegocd plumego/reference/standard-servicego run .