This episode teaches proper configuration management: environment variables with os.Getenv, godotenv for development, Viper for complex needs, a centralized config struct, a custom HTTP server with ReadTimeout and WriteTimeout, and graceful shutdown.

Database credentials, JWT secrets, and server ports must not be hardcoded. Proper configuration lets your application move from development to production without changing a single line of code. This episode teaches the 12-factor app foundations at a practical level.
Episode 10 covers environment variables with os.Getenv, godotenv for development, Viper for complex configuration, a centralized config struct, a custom HTTP server with ReadTimeout and WriteTimeout, and graceful shutdown.
os.Getenv is the most basic approach, and it's actually enough for most applications:
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}Always provide a default. os.Getenv returns an empty string when the variable doesn't exist, so the if port == "" pattern keeps the application running even when configuration is incomplete.
Don't scatter os.Getenv calls across your code. Collect all configuration in a single struct populated once at startup:
type Config struct {
Port string
DatabaseURL string
JWTSecret string
Environment string
}
func Load() Config {
return Config{
Port: getEnv("PORT", "8080"),
DatabaseURL: getEnv("DATABASE_URL", ""),
JWTSecret: getEnv("JWT_SECRET", ""),
Environment: getEnv("ENVIRONMENT", "development"),
}
}This Config struct is then passed to every component through dependency injection — the pattern you built in episode 8.
In development, typing variables in the terminal every time is very inconvenient. godotenv loads a .env file into the environment when the process starts:
go get github.com/joho/godotenvif err := godotenv.Load(); err != nil {
slog.Warn("file .env tidak ditemukan, pakai environment")
}
cfg := Load()Never commit a .env file to the repository — make sure it's in .gitignore. For secrets, always use the system environment or a secret manager.
When configuration grows — YAML files, keys from many sources, and automatic overrides — Viper becomes the choice:
go get github.com/spf13/viperviper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
slog.Warn("konfigurasi file tidak dibaca", "err", err)
}
port := viper.GetString("server.port")With viper.AutomaticEnv(), environment variables automatically override the values from the file. This is a common combination in production.
e.Start uses default settings that are less safe for production. Build your own http.Server so the timeouts are controlled:
s := &http.Server{
Addr: ":" + cfg.Port,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
e.HideBanner = true
e.HidePort = true
if err := e.StartServer(s); err != nil {
e.Logger.Fatal(err)
}ReadTimeout protects against slow connections, WriteTimeout prevents hanging handlers, and IdleTimeout closes inactive connections. e.StartServer(s) runs this custom server together with the Echo router.
A production server must stop gracefully: finish in-flight requests before exiting, rather than cutting connections immediately. Use signal.NotifyContext and e.Shutdown:
ctx, stop := signal.NotifyContext(context.Background(),
os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
if err := e.StartServer(s); err != nil &&
err != http.ErrServerClosed {
e.Logger.Fatal(err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := e.Shutdown(shutdownCtx); err != nil {
e.Logger.Fatal(err)
}
slog.Info("server berhenti dengan rapi")When Ctrl+C or SIGTERM is received, e.Shutdown gives in-flight requests 10 seconds to finish before the process exits. This is a pattern required in every production application.
Episode 10 matures your application configuration: environment variables are read once through a Config struct, godotenv makes development easier, Viper handles complex needs, a custom HTTP server with timeouts protects resources, and graceful shutdown stops the server without cutting off active requests.
Key takeaways:
Config struct.os.Getenv needs a default; never hardcode secrets..env for development; don't commit the file.http.Server sets ReadTimeout and WriteTimeout.signal.NotifyContext and e.Shutdown produce graceful shutdown.In episode 11 next, we'll discuss error handling & logging — HTTPError and a custom error handler, the RFC 9457 Problem Details format for consistent API responses, structured logging with slog via RequestLogger, and log aggregator integration.