Getting Started

Installing nurago, importing a single package, and wiring a full service lifecycle

nurago can be adopted one package at a time or used to wire a whole service. Both paths start from the same module.

Requirements

  • Go 1.26.0 or later (the minimum declared in go.mod; any newer release works).
  • No CGO and no system libraries.

Install

go get github.com/tecnickcom/nurago

Import the packages you need, individually. There is no root package to import and nothing to register at startup:

import (
    "github.com/tecnickcom/nurago/pkg/backoff"
    "github.com/tecnickcom/nurago/pkg/redact"
)

go mod tidy then keeps only the modules your imports actually reach. Importing one package does not pull the dependencies of the others: see /docs/dependency-footprint/.

Using a Single Package

Most packages are self-contained and need nothing else from the module. Redacting a payload before it reaches the logs is a complete use of redact:

safe := redact.Default().String(rawPayload)
logger.Info("request", "payload", safe)

Computing a retry delay is a complete use of backoff:

s := backoff.New(backoff.Config{
    Base:     100 * time.Millisecond,
    Factor:   2,
    Jitter:   50 * time.Millisecond,
    MaxDelay: 30 * time.Second,
})

for {
    // ... attempt the work ...
    time.Sleep(s.Next())
}

Browse /packages/ and take what solves the problem in front of you.

The Options Pattern

Packages that take configuration share one shape: a New constructor with a variadic opts ...Option parameter, and WithXxx functions that set individual fields. Defaults are documented per package and are usable as-is.

c, err := httpclient.New(
    httpclient.WithTimeout(5*time.Second),
    httpclient.WithLogger(logger),
)

Invalid configuration is reported by the constructor as an error rather than a panic, so a misconfigured service fails at startup instead of on the first request.

Wiring a Service Lifecycle

bootstrap.Bootstrap is the entry point for a full service. It takes a BindFunc, which is where your application registers its own components, plus options that tune the runtime:

  1. A cancellable context.Context is created and threaded through the application.
  2. A metrics.Client is created and passed to the bind function. The default is metrics.Default, the no-op implementation, so bootstrap pulls in no metrics backend of its own. Supply a real one with WithCreateMetricsClientFunc.
  3. A *slog.Logger is created and passed to the bind function. With a logutil.Config supplied through WithLogConfig, the logger also emits a metrics counter for every log line, broken down by level.
  4. The bind function is called: register HTTP servers, database connections, and background workers here.
  5. Bootstrap blocks until SIGINT, SIGTERM, or external cancellation of the context.
  6. The shutdown signal is broadcast on the shared channel and the application context is cancelled, so every registered dependent can start its own teardown.
  7. Bootstrap waits on the shared wait group, bounded by the shutdown timeout. If the timeout fires first, it returns an error wrapping ErrShutdownTimeout.
  8. The metrics client is closed so buffered measurements are flushed before the process exits.
func bind(ctx context.Context, l *slog.Logger, m metrics.Client) error {
    // register HTTP servers, workers, DB connections, etc.
    return nil
}

func main() {
    shutdownWG := &sync.WaitGroup{}
    shutdownCh := make(chan struct{})

    err := bootstrap.Bootstrap(
        bind,
        bootstrap.WithLogConfig(logutil.DefaultConfig()),
        bootstrap.WithShutdownTimeout(30*time.Second),
        bootstrap.WithShutdownWaitGroup(shutdownWG),
        bootstrap.WithShutdownSignalChan(shutdownCh),
    )
    if err != nil {
        log.Fatal(err)
    }
}

Bootstrap installs process-global OS signal handling through os/signal.Notify, so it is meant to be called once per process, from main. Supplying a logutil.Config also replaces the process-wide default logger and redirects the standard library log package.

Pass the same shutdown channel and wait group to httpserver, sqlconn, and your own workers. One signal then drains all of them, and the process waits for the slowest up to the budget.

See /packages/bootstrap/.

Serving HTTP

httpserver.New assembles the server from options, a Binder that registers your routes, and a selectable set of built-in operational routes (/ping, /status, /metrics, /pprof/*option, /ip, and a generated route index).

srv, err := httpserver.New(
    ctx,
    &myBinder{},
    httpserver.WithServerAddr(":8080"),
    httpserver.WithEnableDefaultRoutes(httpserver.PingRoute, httpserver.StatusRoute),
    httpserver.WithRequestTimeout(30*time.Second),
    httpserver.WithShutdownTimeout(10*time.Second),
    httpserver.WithLogger(l),
)
if err != nil {
    return err
}

srv.StartServer()

StartServer is non-blocking. Misconfigured routes and options (nil handlers or middleware, duplicate or malformed routes, unknown default route identifiers) are reported by New as wrapped sentinel errors rather than panics. With WithServerAddr(":0") the ephemeral port is readable from srv.Addr().

The default routes expose service internals. Enable them only on a listener that is not reachable from the public internet, or protect them with authentication middleware: see /docs/security/.

Routing stays httprouter; handlers are ordinary net/http handlers. Request parsing and response writing helpers are in /packages/httputil/, and the JSend-style envelope in /packages/jsendx/. Errors can instead be returned as RFC 9457 problem details, with router fallbacks in the same format.

See /packages/httpserver/.

Configuration

config.Load builds the effective configuration from defaults, a local file, environment variables, and an optional remote source, then validates it before the service starts. The application implements two methods: SetDefaults(v Viper) to register defaults, and Validate() error to enforce final constraints.

cfg := &appConfig{}

err := config.Load("examplesrv", configDir, "EXAMPLESRV", cfg)

Later steps override earlier ones: package defaults, then your defaults, then the config file (config.json, searched in the explicit directory first and then in ./, $HOME/.<cmdName>/, /etc/<cmdName>/), then the remote source when one is configured, then environment variables, then Validate().

Only keys registered with a default, or present in the file or remote source, are candidates for environment overrides. A key that has no default and appears nowhere else stays unset even when its environment variable is present. Register a default for every configurable key.

See /packages/config/.

Health Checks

healthcheck runs dependency probes concurrently and aggregates them into one endpoint: 200 when all pass, 503 when any fails, with the per-check results always in the payload. A panicking check is recovered, logged, and reported as failed, and WithTimeout bounds a probe so one hung dependency does not hang the endpoint.

The client packages in this library (redis, valkey, sqs, s3, sqlconn, ipify, slack) expose a HealthCheck method that satisfies the interface directly, so they register without an adapter:

checks := []healthcheck.HealthCheck{
    healthcheck.New("database", dbConn),
    healthcheck.New("cache", redisClient),
}

handler := healthcheck.NewHandler(checks, healthcheck.WithTimeout(time.Second))

See /packages/healthcheck/.

Where to Go Next


Overview: /docs/

Next: /docs/service-scaffolding/

The long form

This page is a reference. For the same ground covered as a narrative, with one service built end to end, Building a Production REST API in Go is a twenty-part guide on tecnick.com.