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):

  1. The signal listener started at Start on both paths catches the signal, logs it, and calls fx.Shutdowner.Shutdown().
  2. fx runs the OnStop hooks. A shutdown context is created with config.ShutdownTimeout (defaulting to 10 seconds when unset or zero).
  3. server.Shutdown(ctx) runs first: the server stops accepting new connections and drains in-flight requests until they finish or the timeout expires.
  4. Every config.OnShutdown hook then runs with the remaining context budget. Errors are logged but do not abort the sequence.
  5. Finally, services registered with di.Service (or app.Register) that implement OnModuleDestroy, plus legacy di.AsDestroyHook values, get their OnModuleDestroy(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: returns 200 {"status":"ok"} whenever the process is alive.
  • GET /ready — readiness: runs your ReadinessCheck; returns 200 {"status":"ready"} on nil, or 503 {"status":"unavailable","reason":"..."} on error. Registered whenever ReadinessCheck is set, independently of HealthCheck.

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.

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
  • ReadTimeout and WriteTimeout set; handler execution bounded with middleware.Timeout()
  • BodyLimit sized for your largest legitimate payload
  • middleware.Recovery() registered first — panics never crash the process
  • middleware.Helmet() for OWASP security headers
  • middleware.CORS() restricted to known origins (never * with credentials)
  • middleware.RateLimit() on public endpoints
  • middleware.CSRF() on cookie-authenticated browser routes
  • config.ValidateFunc set so every DTO is validated on extraction
  • ErrorHandler does not leak internals — consider core.ProblemDetailErrorHandler (RFC 7807)
  • HealthCheck + ReadinessCheck wired to Kubernetes probes
  • ShutdownTimeout shorter than the pod’s terminationGracePeriodSeconds
  • OnShutdown hooks 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