Expert · Building a REST API with a database · topic 38 of 46 · 40 min
Configuration, project structure and graceful shutdown
Explanation
Configuration from the environment
The same binary runs on your laptop, in testing and in production; only the settings differ. The usual approach (from the "twelve-factor app" guidelines) is to read settings from environment variables. Docker, Kubernetes and every cloud platform make them easy to set, and secrets never end up in your code or Git history.
type Config struct {
Port string
DatabaseURL string
LogLevel slog.Level
}
func loadConfig() (Config, error) {
cfg := Config{Port: "8080", LogLevel: slog.LevelInfo}
if v := os.Getenv("PORT"); v != "" {
cfg.Port = v
}
cfg.DatabaseURL = os.Getenv("DATABASE_URL")
if cfg.DatabaseURL == "" {
return cfg, errors.New("DATABASE_URL is required")
}
if os.Getenv("LOG_LEVEL") == "debug" {
cfg.LogLevel = slog.LevelDebug
}
return cfg, nil
}A project structure that scales
expense-api/
├── go.mod
├── main.go config, logger, DB, server, shutdown (wiring only)
├── internal/
│ ├── expense/ types, validation, business rules (no HTTP, no SQL)
│ ├── store/ PGStore: SQL queries (implements the Store interface)
│ └── httpapi/ handlers, middleware, routes, JSON helpers
├── migrations/ 001_create_expenses.sql ...
├── Dockerfile
└── README.mdDependencies flow inwards: httpapi uses expense and a Store interface; store implements it; main connects them. This is enough structure for most services. Add more layers only when a real problem appears.
Graceful shutdown
When you deploy a new version, the platform (for example Kubernetes) sends your process the SIGTERM signal, waits a grace period (30 seconds by default in Kubernetes), and then kills it. If you exit immediately, users in the middle of a payment get errors. A graceful shutdown stops taking new requests, lets in-flight ones finish, and closes resources cleanly.
func main() {
cfg, err := loadConfig()
if err != nil {
slog.Error("config", "err", err)
os.Exit(1)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
db, err := openDB(ctx, cfg.DatabaseURL)
if err != nil {
slog.Error("database", "err", err)
os.Exit(1)
}
defer db.Close()
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: httpapi.New(store.NewPG(db)),
ReadHeaderTimeout: 5 * time.Second,
}
go func() {
slog.Info("listening", "port", cfg.Port)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("server", "err", err)
stop() // trigger shutdown
}
}()
<-ctx.Done() // wait for Ctrl+C or SIGTERM
slog.Info("shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
slog.Error("shutdown", "err", err)
}
slog.Info("bye")
}- signal.NotifyContext (Go 1.16+) gives a context that is cancelled when the signal arrives.
- srv.Shutdown closes the listeners, waits for active requests to finish, or gives up when its context expires.
- Use a NEW context (from Background) for Shutdown; the signal context is already cancelled.
- Keep the shutdown timeout below the platform's grace period.
- Background workers should watch the same ctx and stop their loops when it is done.
Libraries you will meet
| Need | Standard library | Popular options |
|---|---|---|
| Routing | net/http ServeMux (Go 1.22+) | chi (stdlib-compatible), gin, echo |
| SQL | database/sql | pgx (PostgreSQL), sqlc (generated code), GORM (ORM) |
| Config | os.Getenv | envconfig-style libraries, viper |
| Validation | your own checks | go-playground/validator |
| Testing helpers | testing, httptest | testify |
You can build a complete production API with only the standard library plus a database driver. Teams often add chi or gin for convenience and sqlc for type-safe SQL. Learn the standard library first; every library above builds on it, and interviewers will expect you to understand it.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Configuration, project structure and graceful shutdown · 1/7
A production service needs settings, a clear structure, and a clean way to stop.
Prefer a video by a teacher? Find free videos on “golang graceful shutdown signal.NotifyContext http server shutdown environment config” on YouTube ↗
In real life: Zero-downtime deploys on Kubernetes
A ticket-booking API runs 3 copies (pods) on Kubernetes. During a deploy, each old pod gets SIGTERM. Thanks to graceful shutdown, it stops accepting new connections, finishes the bookings already in progress within a few seconds, closes its database pool and exits. Users notice nothing, even though the whole service was replaced during the evening rush.
# what the platform does, step by step
1. New pod starts and passes its readiness check (/healthz)
2. Old pod is removed from the load balancer
3. Old pod receives SIGTERM
4. Go: ctx.Done() fires → srv.Shutdown(20s) → in-flight requests finish
5. Go: db.Close() → process exits with code 0
6. (If still running after the grace period, the pod is killed)Practice (0/4 done)
After srv.Shutdown, what does srv.ListenAndServe return?
Which signal do Docker and Kubernetes send first to stop a container?
Where should a production database password come from?
Take your expense API and add: loadConfig from env (PORT default 8080, DATABASE_URL required), signal.NotifyContext, an http.Server with timeouts started in a goroutine, and a 15-second graceful Shutdown. Add a handler that sleeps 5 seconds, call it with curl, press Ctrl+C immediately, and confirm the request still completes.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Middleware: logging, recovery and request IDsBuilding a REST API with a database · same module
- REST API design and JSON handlersBuilding a REST API with a database · same module
- Your portfolio project: a production-style Go APICareers and interviews
- Environment variables and secretsFull-Stack Development · Deploying and running the app
- Environment variables and configBackend Development · Node.js and Express
- Processes, signals and systemd servicesDevOps: Git, Docker, Kubernetes and CI/CD · Linux for DevOps
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What does srv.Shutdown do?
Answer: Stops accepting new connections and waits for active requests to finish (or the context to expire)
It is the graceful counterpart to Close.
Why use a new context for Shutdown instead of the signal context?
Answer: The signal context is already cancelled, so Shutdown would give up immediately
A fresh timeout context gives requests time to finish.
Which package should contain business rules like "amount must be positive"?
Answer: A domain package like internal/expense with no HTTP or SQL code
Pure business logic is easiest to test on its own.
Can you build a production API with only the standard library plus a DB driver?
Answer: Yes; frameworks are optional conveniences
net/http, encoding/json and database/sql cover the essentials.
Where should database passwords for your project live?Related
Answer: In environment variables, never committed
Commit an example config without real secrets, if anything.
A developer adds VITE_DATABASE_URL to the React app. What is the problem?Related
Answer: The database URL, with its password, ends up in the JavaScript that every visitor downloads
VITE_ variables are pasted into the browser bundle at build time, so anyone can read them in DevTools.
Where does Node.js keep environment variables?Related
Answer: In process.env
Every environment variable is a property of the process.env object.
Which command shows the logs of the service orders-api and keeps following new lines?Related
Answer: journalctl -u orders-api -f
journalctl -u selects the unit and -f follows new entries.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- This is the last topic of “Building a REST API with a database”. The module exam comes next.
- Module 8 exam: Building a REST API with a database · 14 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, CLI tools and Go in cloud and DevOps. 46 topics left in the course before the final exam.