Production Guide: Running a NestGo API in Production
A practical Go API production checklist for NestGo: configure timeouts and TLS, wire graceful shutdown, expose Kubernetes health checks, and plug in structured logging. Everything on this page maps directly to fields on core.Config and hooks in core.Config — applied identically by app and di.
Config field reference
core.Config holds all framework-level configuration. core.DefaultConfig() returns development-friendly defaults:
config := core.DefaultConfig()
// AppName: "NestGo App", Addr: ":8080", Adapter: "gin",
// BodyLimit: 4 MiB, ReadTimeout: 10, WriteTimeout: 10,
// ReadHeaderTimeout: 10, IdleTimeout: 60, MaxHeaderBytes: 1 MiB, Debug: false
Every field, its type, default, and purpose:
| Field | Type | Default | Purpose |
|---|---|---|---|
AppName | string | "NestGo App" | Application name, used for identification |
Addr | string | ":8080" | Listen address for the HTTP server |
Adapter | string | "gin" | Adapter name (informational; the actual engine comes from the ServerProvider you pass to di.NewApp) |
GlobalPrefix | string | "" | Path prefix applied to all routes (e.g. /api) |
BodyLimit | int | 4 * 1024 * 1024 | Max request body size in bytes, enforced at the transport level by both adapters — Gin via http.MaxBytesReader (413 on excess), Fiber natively; use middleware.BodyLimit() for per-route limits |
ReadTimeout | int | 10 | HTTP server read timeout, in seconds |
WriteTimeout | int | 10 | HTTP server write timeout, in seconds |
ReadHeaderTimeout | int | 10 | Max time reading request headers, in seconds — slowloris protection (not mappable on the Fiber adapter) |
IdleTimeout | int | 60 | Max time an idle keep-alive connection stays open, in seconds |
MaxHeaderBytes | int | 1 << 20 (1 MiB) | Max request header size, in bytes (not mappable on the Fiber adapter) |
Debug | bool | false | Debug mode — keep false in production |
DisableLogger | bool | false | Disables the adapter’s built-in request logging |
ErrorHandler | core.ErrorHandler | core.DefaultErrorHandler | Global error handler; core.ProblemDetailErrorHandler emits RFC 7807 responses |
TLSCertFile | string | "" | Path to the TLS certificate — set together with TLSKeyFile to enable HTTPS |
TLSKeyFile | string | "" | Path to the TLS private key |
GlobalMiddlewares | []core.MiddlewareFunc | nil | Middleware applied to all routes (outermost) |
GlobalGuards | []core.Guard | nil | Guards applied to all routes |
GlobalPipes | []core.MiddlewareFunc | nil | Pipes applied to all routes |
GlobalInterceptors | []core.Interceptor | nil | Interceptors applied to all routes |
GlobalFilters | []core.ExceptionFilter | nil | Exception filters applied to all routes |
ValidateFunc | func(v interface{}) error | nil | Global DTO validation, called by B[T]() / QDto[T]() after binding (e.g. validator.Validate) |
RequestTimeout | time.Duration | 0 (no timeout) | Max handler execution time — when > 0, di.NewApp applies middleware.Timeout() globally (504 on deadline). Leave 0 for apps with SSE/streaming routes and use per-route middleware.Timeout() with a SkipFunc instead |
Versioning | *core.VersioningConfig | nil | API versioning strategy (URI, Header, or Media Type) |
HealthCheck | bool | false | Enables the built-in GET /health liveness endpoint |
ReadinessCheck | func() error | nil | Called by GET /ready; return nil when ready, an error for 503 |
ShutdownTimeout | time.Duration | 0 (treated as 10s) | How long to drain in-flight requests before forcing shutdown |
OnShutdown | []func(ctx context.Context) error | nil | Hooks that run before the server stops (close pools, flush queues) |
Execution order for the global features mirrors NestJS: Middleware → Filters → Guards → Pipes → Interceptors → Handler.
How does graceful shutdown work in Go with NestGo?
NestGo wires graceful shutdown automatically on both the app and di paths — no manual signal.Notify needed. The flow when the process receives SIGINT or SIGTERM (e.g. a Kubernetes pod termination):
- The signal listener started at Start on both paths catches the signal, logs it, and calls
fx.Shutdowner.Shutdown(). - fx runs the
OnStophooks. A shutdown context is created withconfig.ShutdownTimeout(defaulting to 10 seconds when unset or zero). server.Shutdown(ctx)runs first: the server stops accepting new connections and drains in-flight requests until they finish or the timeout expires.- Every
config.OnShutdownhook then runs with the remaining context budget. Errors are logged but do not abort the sequence. - Finally, services registered with
di.Service(orapp.Register) that implementOnModuleDestroy, plus legacydi.AsDestroyHookvalues, get theirOnModuleDestroy(ctx)called — dependencies close only after no requests are being served.
fx’s overall stop budget is extended automatically: when ShutdownTimeout is set, NestGo applies fx.StopTimeout(ShutdownTimeout + 5s), so a long drain window is never silently truncated by fx’s 15-second default.
Startup failures are handled symmetrically: if the server cannot listen (port already in use, bad certificate), the error is logged and the app shuts down with exit code 1 instead of idling as a zombie process with no listener.
Configure it in one place:
config := core.DefaultConfig()
config.ShutdownTimeout = 30 * time.Second
config.OnShutdown = []func(ctx context.Context) error{
func(ctx context.Context) error { db.Close(); return nil },
func(ctx context.Context) error { return queue.Flush(ctx) },
}
On Kubernetes, make sure terminationGracePeriodSeconds (default 30) is larger than your ShutdownTimeout, so the kubelet does not SIGKILL the pod mid-drain.
TLS: serving HTTPS from a Go server
Set both TLSCertFile and TLSKeyFile and NestGo automatically starts the server with StartTLS instead of plain Start:
config := core.DefaultConfig()
config.Addr = ":8443"
config.TLSCertFile = "/etc/certs/tls.crt"
config.TLSKeyFile = "/etc/certs/tls.key"
app := di.NewApp(config, ginadapter.New, users.Module)
app.Run()
If both fields are empty, the server listens over plain HTTP. Setting only one of them is a configuration error: startup fails with a clear message instead of silently serving cleartext HTTP while you believe TLS is on. Behind a TLS-terminating load balancer or ingress, leave both empty and terminate TLS at the edge.
Health checks and readiness probes for Kubernetes
NestGo registers Kubernetes-friendly probe endpoints from config — no controller needed:
config.HealthCheck = true // GET /health → 200 {"status":"ok"}
config.ReadinessCheck = func() error { // GET /ready
return db.Ping() // nil → 200, error → 503 with reason
}
GET /health— liveness: returns200 {"status":"ok"}whenever the process is alive.GET /ready— readiness: runs yourReadinessCheck; returns200 {"status":"ready"}onnil, or503 {"status":"unavailable","reason":"..."}on error. Registered wheneverReadinessCheckis set, independently ofHealthCheck.
Both endpoints are registered on the server root, outside GlobalPrefix.
Matching Kubernetes probe configuration:
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
Timeouts and request limits
Slowloris-style attacks and oversized payloads are contained by a handful of settings:
config.ReadTimeout = 15 // seconds — reading the request
config.WriteTimeout = 15 // seconds — writing the response
config.ReadHeaderTimeout = 10 // seconds — reading request headers (slowloris protection)
config.IdleTimeout = 60 // seconds — idle keep-alive connections
config.MaxHeaderBytes = 1 << 20 // 1 MiB max request headers
config.BodyLimit = 2 * 1024 * 1024 // 2 MiB max request body
To bound handler execution time globally, set config.RequestTimeout — di.NewApp applies the Timeout middleware for you:
config.RequestTimeout = 30 * time.Second
Note: the global timeout also applies to long-lived routes (SSE, streaming). For apps with such routes, leave RequestTimeout at 0 and register middleware.Timeout() with a SkipFunc that exempts them:
server.Use(middleware.Timeout(middleware.TimeoutConfig{
Timeout: 30 * time.Second,
SkipFunc: func(c core.Context) bool { return strings.HasPrefix(c.Path(), "/events") },
}))
For per-route control, the middleware package provides middleware.Timeout() and middleware.BodyLimit():
r.POST("/upload", handler, middleware.Timeout(middleware.TimeoutConfig{
Timeout: 60 * time.Second,
}))
middleware.Timeout uses context.WithTimeout, so downstream DB, gRPC, and HTTP calls that receive the request context (via the core.RCtx() extractor) are cancelled automatically.
Structured logging with SetLogger
NestGo logs through a small core.Logger interface, so you can plug in zap, slog, zerolog, or logrus. Call core.SetLogger in main() before starting the app:
type Logger interface {
Debug(msg string, fields ...Field)
Info(msg string, fields ...Field)
Warn(msg string, fields ...Field)
Error(msg string, fields ...Field)
}
A zap adapter:
type zapAdapter struct{ l *zap.Logger }
func (a *zapAdapter) fields(fs []core.Field) []zap.Field {
out := make([]zap.Field, 0, len(fs))
for _, f := range fs {
out = append(out, zap.Any(f.Key, f.Value))
}
return out
}
func (a *zapAdapter) Debug(msg string, fs ...core.Field) { a.l.Debug(msg, a.fields(fs)...) }
func (a *zapAdapter) Info(msg string, fs ...core.Field) { a.l.Info(msg, a.fields(fs)...) }
func (a *zapAdapter) Warn(msg string, fs ...core.Field) { a.l.Warn(msg, a.fields(fs)...) }
func (a *zapAdapter) Error(msg string, fs ...core.Field) { a.l.Error(msg, a.fields(fs)...) }
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()
core.SetLogger(&zapAdapter{l: logger})
// ...
}
For simple cases, the functional adapter core.LoggerFunc maps all levels to one function — here bridging to log/slog:
core.SetLogger(core.LoggerFunc(func(level, msg string, fields []core.Field) {
attrs := make([]any, 0, len(fields)*2)
for _, f := range fields {
attrs = append(attrs, f.Key, f.Value)
}
switch level {
case "ERROR":
slog.Error(msg, attrs...)
case "WARN":
slog.Warn(msg, attrs...)
case "DEBUG":
slog.Debug(msg, attrs...)
default:
slog.Info(msg, attrs...)
}
}))
Framework internals (controller registration, shutdown signals, lifecycle hook errors) all log through core.Log(), so one SetLogger call gives you consistent structured output everywhere.
Recommended production middleware stack
Register middleware before your route modules so it wraps every route (see Dependency Injection for why ordering matters):
app := di.NewApp(config, ginadapter.New,
di.Invoke(func(server core.Server) {
server.Use(middleware.Recovery()) // panic recovery — always first
server.Use(middleware.RequestID()) // unique ID per request
server.Use(middleware.Tracing()) // W3C traceparent propagation
server.Use(middleware.Logger(middleware.LoggerConfig{
SkipFunc: func(c core.Context) bool {
return c.Path() == "/health" || c.Path() == "/ready"
},
}))
server.Use(middleware.Helmet()) // OWASP security headers
server.Use(middleware.CORS()) // lock down origins in production
server.Use(middleware.RateLimit(middleware.RateLimitConfig{
Max: 200,
Window: 5 * time.Minute,
}))
}),
users.Module,
)
Production security checklist
Debug = false- TLS enabled (
TLSCertFile/TLSKeyFile) or terminated at the load balancer ReadTimeoutandWriteTimeoutset; handler execution bounded withmiddleware.Timeout()BodyLimitsized for your largest legitimate payloadmiddleware.Recovery()registered first — panics never crash the processmiddleware.Helmet()for OWASP security headersmiddleware.CORS()restricted to known origins (never*with credentials)middleware.RateLimit()on public endpointsmiddleware.CSRF()on cookie-authenticated browser routesconfig.ValidateFuncset so every DTO is validated on extractionErrorHandlerdoes not leak internals — considercore.ProblemDetailErrorHandler(RFC 7807)HealthCheck+ReadinessCheckwired to Kubernetes probesShutdownTimeoutshorter than the pod’sterminationGracePeriodSecondsOnShutdownhooks close DB pools, flush queues, and stop background workers- Structured logger plugged in via
core.SetLogger middleware.Idempotency()on payment or retry-sensitive mutation endpoints
Next steps
- Dependency Injection — modules, lifecycle hooks, and why middleware must register before routes
- Middleware — full reference for the built-in middleware
- Guards — authentication and RBAC before handlers run
Source: github.com/ashrafAli23/nestgo