Skip to content

Expert · Building a REST API with a database · topic 38 of 46 · 40 min

Configuration, project structure and graceful shutdown

Explanation

Read configuration (port, database URL, log level) from environment variables, check it at start-up and fail fast if something required is missing. Organise the service as main plus a few internal packages. Shut down gracefully: catch SIGTERM with signal.NotifyContext, stop accepting new requests with server.Shutdown, let running ones finish, then close the database.

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.

go
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
}
Defaults for optional settings, a clear error for required ones. os.LookupEnv tells "set to empty" apart from "not set" when that matters.
Careful: Never commit secrets (database passwords, API keys) to Git, and never print the whole config at start-up. Go does not read .env files on its own; libraries like godotenv can load one for local development, but production should use real environment variables or a secrets manager.

A project structure that scales

output
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.md
Each package has one job. Business rules can be tested without a database or HTTP.

Dependencies 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.

go
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")
}
ListenAndServe returns http.ErrServerClosed after Shutdown; that is the normal case, not an error. The deferred db.Close runs after the server has stopped.
  • 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

NeedStandard libraryPopular options
Routingnet/http ServeMux (Go 1.22+)chi (stdlib-compatible), gin, echo
SQLdatabase/sqlpgx (PostgreSQL), sqlc (generated code), GORM (ORM)
Configos.Getenvenvconfig-style libraries, viper
Validationyour own checksgo-playground/validator
Testing helperstesting, httptesttestify

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

Config · structure · shutdown

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.

output
# 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)
Graceful shutdown is what makes step 4 safe.

Practice (0/4 done)

Task 1 · Question

After srv.Shutdown, what does srv.ListenAndServe return?

Task 2 · Question

Which signal do Docker and Kubernetes send first to stop a container?

Task 3 · Question

Where should a production database password come from?

Task 4 · Practice

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

Question 1

1.What does srv.Shutdown do?

Question 2

2.Why use a new context for Shutdown instead of the signal context?

Question 3

3.Which package should contain business rules like "amount must be positive"?

Question 4

4.Can you build a production API with only the standard library plus a DB driver?

Read more (free)

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.

From Your portfolio project: a production-style Go API

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.

From Environment variables and secrets

Where does Node.js keep environment variables?Related

Answer: In process.env

Every environment variable is a property of the process.env object.

From Environment variables and config

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.

From Processes, signals and systemd services

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

  1. This is the last topic of “Building a REST API with a database”. The module exam comes next.
  2. 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.
  3. Then the next module, CLI tools and Go in cloud and DevOps. 46 topics left in the course before the final exam.

← Previous: Databases with database/sql