credo

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 48 Imported by: 0

README

Credo

An all-in-one Go web framework for modern, enterprise-grade applications.

Go Version License Go Report Card GoDoc

Status: Beta -- Core APIs are stable enough for real application development. Breaking changes may still happen before v1, but should be documented with migration notes. Feedback from active pilot projects continues to shape the framework.

What Credo Optimizes For

  • All-in-one: router, DI, config, validation, observability, data access -- one cohesive framework.
  • Enterprise defaults: Clean Architecture is the recommended path (CLI scaffolding + docs), but not enforced.
  • Errors are values: handlers return error; centralized error handling renders RFC 7807 Problem Details.
  • Typed config snapshot: config is loaded once at startup and injected as typed structs via DI.
  • Integrated-first, explicit boundaries: framework infrastructure is wired by convention; app deps and typed config stay visible and override-friendly.
  • stdlib boundary compatibility: *credo.App is an http.Handler; stdlib middleware can be adapted.

Why Credo Exists

Go teams often assemble production web applications from many excellent but separate libraries: routing, configuration, dependency injection, validation, observability, data access, workers, and middleware. Credo packages those concerns into one integrated framework so teams can spend less time maintaining glue code and more time building application logic.

The project is early but actively maintained, with a focus on clear APIs, documented architecture decisions, security-conscious upstream adaptation, and production-oriented defaults for modern Go services.

Maturity by Area

Credo is Beta overall. Shipped packages are usable for real development; the table below is explicit about what is solid, what is still experimental, and what is on the roadmap.

Area Status
Routing, Context, Handlers, Middleware Beta
Dependency Injection (Provide/Resolve, Infra) Beta
Configuration (config, typed snapshot) Beta
Validation Beta
Authentication (JWT / Basic / API key) Beta
Internationalization (i18n) Beta
Health checks Beta
Data access — contracts (store) Beta
Data access — Bun SQL wrapper + migrations (store/sqldb) Beta
Background workers + cron (worker) Beta
Pagination Beta
Outbound HTTP client (httpclient) Beta
Server WebSocket adapter (websocket) Beta
Observability — structured logging (slog via Infra) Beta
Observability — tracing (OpenTelemetry) Experimental
Observability — metrics (Prometheus) Experimental
pubsub (incl. in-process events) · grpc · openapi · admin server · CLI Planned

Installation

go get github.com/credo-go/credo@latest

Requires Go 1.27+. Credo tracks the current Go release to build on the modern standard library (e.g. os.Root, structured log/slog) and language features — the 1.27 floor is driven by generic methods (app.Provide[T], q.Page[T], ctx.GetUser[T]). It targets new and actively-maintained services rather than legacy codebases pinned to older toolchains — enterprise-grade in capability, modern in its baseline.

Quick Start (Target API)

package main

import (
    "log"

    "github.com/credo-go/credo"
)

func main() {
    // Create the app (auto-loads config from files, .env, and env vars).
    app, err := credo.New()
    if err != nil {
        log.Fatal(err)
    }

    app.GET("/", func(ctx *credo.Context) error {
        return ctx.Response().JSON(200, map[string]string{"message": "Hello, Credo!"})
    })

    // Every request is access-logged out of the box. Silence one route or group
    // with .SetMeta(credo.MetaAccessLog, false), or a noisy path with
    // credo.WithAccessLogSkipper(...). Health probes are silent by default.

    // Server settings come from framework-internal server config.
    // Example: set `CREDO_SERVER__PORT=8080`.
    //
    // Run blocks until SIGINT/SIGTERM, then drains in-flight requests
    // gracefully. For caller-driven cancellation (tests, custom signals),
    // use app.RunContext(ctx) instead.
    //
    // For HTTPS, configure TLS at construction — credo.WithTLSFiles(cert, key)
    // or credo.WithTLSConfig(cfg) (or the server.tls.* config keys) — and Run
    // serves TLS automatically; there is no separate RunTLS. Add
    // credo.WithHTTPRedirect(":80") to redirect plaintext HTTP to HTTPS.
    if err := app.Run(); err != nil {
        log.Fatal(err)
    }
}

For explicit config control, load it first with config.Load(...) and pass it to credo.New(credo.WithRawConfig(raw)); this bypasses the default auto-load.

Config: Typed Snapshot (Anti-Pattern-Free)

Credo positions config as a startup-time snapshot, not a runtime service. String keys should appear only at module boundaries; everything beyond that boundary is typed.

type DatabaseConfig struct {
    DSN string
}

func SetupDatabase(app *credo.App, rawCfg credo.RawConfig) error {
    var cfg DatabaseConfig
    if err := rawCfg.Unmarshal("databases.default", &cfg); err != nil {
        return err
    }
    return app.ProvideValue(&cfg)
}

Field names map to snake_case config keys automatically, so struct tags are optional — add a credo:"..." tag only when the key differs from the field's snake_case name.

Dependency Injection

Credo uses generics-based DI. Cross-cutting infrastructure is carried explicitly via credo.Infra; business dependencies (including typed config) are normal constructor parameters. Single-interface wiring uses Alias[I, T], and ordered interface collections use BindMany[I, T] + ResolveAll[I] or []I constructor injection.

func NewOrderService(infra credo.Infra, cfg *OrderConfig, repo OrderRepo) *OrderService {
    infra.Logger.Info("order service initialized")
    return &OrderService{cfg: cfg, repo: repo}
}

Documentation

  • User guide: docs/guides/getting-started.md
  • User guide: docs/guides/routing.md
  • User guide: docs/guides/middleware.md
  • User guide: docs/guides/proxy-trust.md
  • User guide: docs/guides/dependency-injection.md
  • User guide: docs/guides/data-access.md
  • User guide: docs/guides/configuration.md
  • User guide: docs/guides/localization.md
  • User guide: docs/guides/websocket.md
  • Architecture decisions: docs/adr/
  • Detailed specs: docs/specs/

Repository Layout (High Level)

  • Root package (github.com/credo-go/credo): App, Context, routing, handler/middleware types
  • config/: config loading; config.Load returns *config.Config (satisfies credo.RawConfig), with typed Get[T] access
  • middleware/, validation/, auth/, store/, ...: feature packages
  • internal/: private implementations (router radix tree, DI internals, etc.)
  • docs/: ADRs (docs/adr/) and detailed specs (docs/specs/)

Contributing

See CONTRIBUTING.md for guidelines on how to contribute.

Security

See SECURITY.md for vulnerability reporting instructions, and SECURITY-UPSTREAMS.md for how Credo tracks the upstream projects its adapted code derives from. Reports are triaged by severity on a best-effort basis — there is no fixed response-time guarantee.

License

MIT -- see LICENSE for details.

Documentation

Overview

Package credo is a batteries-included Go web framework that combines the best patterns from Chi (router), Echo (context), Goyave (architecture & components), and GoFr (enterprise toolkit).

It targets Go 1.27+ and leverages generics for type-safe dependency injection without reflection.

Quick Start

package main

import (
	"log"

	"github.com/credo-go/credo"
)

func main() {
    app, err := credo.New()
    if err != nil {
        panic(err)
    }

    app.GET("/", func(ctx *credo.Context) error {
        return ctx.Response().JSON(200, map[string]string{"message": "Hello, Credo!"})
    })

    if err := app.Run(); err != nil {
        log.Fatal(err)
    }
}

Key Concepts

  • Handler: func(*credo.Context) error — all handlers return errors
  • Context: request-scoped struct with Request/Response accessors
  • Middleware: func(credo.Handler) credo.Handler — wraps Handlers. Four tiers run in order: built-in → global → group → route. Group middleware is collected from the group parent chain when the app compiles, so registration order affects execution order only — middleware added to a group after its routes still applies to them.
  • Route: fluent API with Name(), SetMeta(), Middleware()
  • ErrorRenderer: customizes error response formatting via App.SetErrorRenderer (classification/logging handled by framework)
  • SuccessRenderer: opt-in uniform success envelope via App.SetSuccessRenderer, applied only at the Context.Render seam (raw Response helpers stay un-enveloped)

API Naming

Credo uses a consistent verb convention so a method's name signals when and how it runs:

Request logging is on by default (see WithLogger). Silence individual routes or whole groups with the MetaAccessLog route meta, or noisy paths with WithAccessLogSkipper; health probes are silent by default (HealthConfig.LogRequests re-enables them).

Panics and Errors

Credo separates developer errors from runtime failures:

  • Startup configuration (registering routes, hosts, middleware, names, static files, health checks) panics on misuse — nil handlers, malformed patterns, duplicates, or registration after the handler chain has compiled. The route table is code written by the developer, so a mistake there is a bug best caught at startup, not a condition to handle.
  • Anything that can legitimately fail at runtime — request handling, server lifecycle, or operations touching the outside world (file I/O, network) — returns an error.

This is why App.UseHealth panics on misuse (it only registers in-process state) while App.UseI18n returns an error (it loads locale files).

Stability

Credo is Beta: shipped packages are usable for real development, with breaking changes possible before v1. See the project README's "Maturity by Area" table for per-area status, including which features are experimental or still planned.

Index

Constants

View Source
const (
	MsgKeyBadRequest          = "http.bad_request"
	MsgKeyUnauthorized        = "http.unauthorized"
	MsgKeyForbidden           = "http.forbidden"
	MsgKeyNotFound            = "http.not_found"
	MsgKeyMethodNotAllowed    = "http.method_not_allowed"
	MsgKeyConflict            = "http.conflict"
	MsgKeyUnprocessableEntity = "http.unprocessable_entity"
	MsgKeyUnsupportedMedia    = "http.unsupported_media_type"
	MsgKeyInternalError       = "http.internal_server_error"
	MsgKeyTooManyRequests     = "http.too_many_requests"
	MsgKeyServiceUnavailable  = "http.service_unavailable"
	MsgKeyGatewayTimeout      = "http.gateway_timeout"
	MsgKeyRequestTimeout      = "http.request_timeout"
	MsgKeyValidationFailed    = "http.validation_failed"
)

MsgKey constants define i18n message keys for standard HTTP errors. These keys are used in locale files (e.g., locales/en/messages.json) and as lookup keys for [builtInMessages].

View Source
const MetaAccessLog = "credo.accesslog"

MetaAccessLog is the route-meta key that toggles built-in access logging for a route — or, via LookupMeta parent-chain inheritance, for a whole group. Set it to false to silence the access-log line for matched requests:

app.Group("/internal").SetMeta(credo.MetaAccessLog, false)
app.GET("/metrics", h).SetMeta(credo.MetaAccessLog, false)

A route-level value overrides a group-level one (LookupMeta reads the route before its parents), so a noisy group can be silenced while one route inside it stays logged. Only a bool false silences; any non-bool value is ignored and the request is logged (fail-open). Keys under the "credo." namespace are reserved by the framework.

This key is honoured by both the built-in access logger and [middleware.AccessLog]. Health probes use it internally; see HealthConfig.LogRequests.

Variables

View Source
var (
	ErrNotFound             = NewHTTPError(http.StatusNotFound)
	ErrMethodNotAllowed     = NewHTTPError(http.StatusMethodNotAllowed)
	ErrBadRequest           = NewHTTPError(http.StatusBadRequest)
	ErrUnauthorized         = NewHTTPError(http.StatusUnauthorized)
	ErrForbidden            = NewHTTPError(http.StatusForbidden)
	ErrInternalServerError  = NewHTTPError(http.StatusInternalServerError)
	ErrConflict             = NewHTTPError(http.StatusConflict)
	ErrUnprocessableEntity  = NewHTTPError(http.StatusUnprocessableEntity)
	ErrUnsupportedMediaType = NewHTTPError(http.StatusUnsupportedMediaType)
)

Sentinel errors for common HTTP error conditions.

These are shared package-level instances, like io.EOF: compare with errors.Is and treat them as immutable. Mutating a sentinel's fields would silently change the behavior of every handler in the process. To attach context, derive a copy instead — HTTPError.WithInternal for a wrapped cause, or NewHTTPError for a different status or message key.

View Source
var ErrUserMissing = errors.New("credo: no user in context (or type mismatch)")

ErrUserMissing is returned by Context.RequireUser when no authenticated user of the requested type is present on the request. RequireUser wraps it inside ErrUnauthorized, so a handler that returns the error renders a 401 while errors.Is(err, ErrUserMissing) still reports the underlying cause.

Functions

func DirFS

func DirFS(dir string) (fs.FS, io.Closer, error)

DirFS returns a traversal-safe fs.FS rooted at dir, together with an io.Closer that releases the underlying directory handle.

Unlike os.DirFS, it is backed by os.Root, so symlinks that resolve outside dir are refused — closing a common path-traversal hole when serving files from disk. Prefer it over os.DirFS for disk-backed static serving.

The FS holds an open directory handle until the closer is called. Register the closer for graceful shutdown so the handle is released cleanly:

fsys, closer, err := credo.DirFS("./public")
if err != nil {
	return err
}
app.Static("/assets", fsys)
app.OnShutdown(func(context.Context) error { return closer.Close() })

It returns an error if dir cannot be opened.

func StaticCacheImmutableAssets

func StaticCacheImmutableAssets(maxAge time.Duration) func(StaticCacheContext) string

StaticCacheImmutableAssets returns a StaticConfig.CacheControl hook for content-hashed builds: non-HTML assets get "public, max-age=N, immutable", while HTML responses (index files, SPA fallbacks, prerendered pages, and directory listings) get "no-cache, must-revalidate" so entry points stay refreshable. Panics if maxAge is negative or floors below 1 second.

func StaticCacheMaxAge

func StaticCacheMaxAge(maxAge time.Duration) func(StaticCacheContext) string

StaticCacheMaxAge returns a StaticConfig.CacheControl hook that serves every successful response with "public, max-age=N". Sub-second durations are floored to whole seconds; if the floored result is 0 the hook writes no header. Panics if maxAge is negative.

func URLParam

func URLParam(r *http.Request, name string) string

URLParam returns the named URL parameter from the request's RouteContext. It is intended for use in stdlib handlers mounted via App.Mount; normal Credo handlers should use Request.RouteParams instead. Returns empty string if the parameter doesn't exist or no RouteContext is set.

func Walk

func Walk(r Routes, fn WalkFunc) error

Walk iterates over registered routes, calling fn with the method and pattern of each one. Mounts (Kind == RouteKindMount) are skipped: the method+pattern shape cannot represent a mount, which answers many methods at a prefix. Use WalkRoutes for the full surface, mounts included.

func WalkRoutes

func WalkRoutes(r Routes, fn WalkRoutesFunc) error

WalkRoutes iterates over all registered routes with full RouteInfo, calling fn for each one.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App is the main Credo application. It holds the router, middleware stack, context pool, error handler, and route registry.

func New

func New(opts ...Option) (*App, error)

New creates a new App with the given options. When no RawConfig is provided, New auto-loads configuration with config.Load and registers the resulting RawConfig in DI. Passing WithRawConfig bypasses auto-load and registers the given RawConfig instead.

New returns an error if configuration loading fails or if server settings contain invalid values (negative timeouts, invalid port).

Usage:

// Zero-config (all defaults):
app, err := credo.New()

// With listen address:
app, err := credo.New(credo.WithAddr("127.0.0.1", 8080))

// With explicit RawConfig (server settings read from "server" key):
app, err := credo.New(credo.WithRawConfig(rawCfg))

func (*App) AddLivenessCheck

func (app *App) AddLivenessCheck(name string, checker HealthChecker)

AddLivenessCheck registers a named liveness check. Panics if App.UseHealth has not been called first, checker is nil, or name is empty, padded with whitespace, contains control characters, uses the reserved "credo." prefix, or duplicates another liveness check.

func (*App) AddReadinessCheck

func (app *App) AddReadinessCheck(name string, checker HealthChecker)

AddReadinessCheck registers a named readiness check. Panics if App.UseHealth has not been called first, checker is nil, or name is empty, padded with whitespace, contains control characters, uses the reserved "credo." prefix, or duplicates another readiness check.

func (*App) Addr

func (app *App) Addr() net.Addr

Addr returns the actual network address the server is listening on. This is particularly useful when the server was started with port 0, as the OS assigns an ephemeral port. Returns nil before Run or after the server stops.

func (*App) Alias

func (app *App) Alias[I, T any]() error

Alias creates a type alias so that resolving interface I via App.Resolve returns the singleton registered for concrete type T. I must be an interface, T must implement I, and T must already be registered.

app.Alias[UserRepo, *PgUserRepo]()

func (*App) BindMany

func (app *App) BindMany[I, T any]() error

BindMany adds concrete type T to the ordered collection for interface I. I must be an interface, T must be a registered concrete type, and T must implement I.

func (*App) CanProvideValue

func (app *App) CanProvideValue[T any]() error

CanProvideValue reports whether App.ProvideValue could currently register type T. It checks only whether the DI container is finalized or T already has a direct registration, and does not mutate or reserve the registration.

The result is a point-in-time preflight. A later ProvideValue call can still fail if another registration or finalization occurs in between.

func (*App) DELETE

func (app *App) DELETE(pattern string, h Handler) *Route

DELETE registers a DELETE route. Panics under the same conditions as App.GET.

func (*App) File

func (app *App) File(urlPath string, fsys fs.FS, name string, cfgs ...StaticConfig) *Route

File registers a single GET route that serves one named file from fsys. Returns *Route for standard fluent chaining (Name, SetMeta, Middleware).

Only the Download and CacheControl fields of StaticConfig are supported. Setting Browse, SPA, or a non-empty Index panics at registration time.

Panics if called after compile.

func (*App) Finalize

func (app *App) Finalize() error

Finalize freezes the DI container and validates the dependency graph. After Finalize, no more Provide, ProvideFactory, ProvideValue, ProvideProtectedValue, ProtectBinding, Replace, Alias, or BindMany calls are allowed. Finalize is idempotent. If not called explicitly, the Run* entry points call it implicitly.

app.Finalize()

func (*App) GET

func (app *App) GET(pattern string, h Handler) *Route

GET registers a GET route. A matching HEAD handler is registered automatically; subsequent calls to Route.Middleware and Route.SetMeta on the returned route apply to HEAD as well, so HEAD requests can never silently bypass auth, rate limiting, or meta-driven middleware.

Panics if h is nil, the pattern is invalid or already registered, or if called after compile (route registration is developer configuration — see the package-level "Panics and Errors" section).

func (*App) GetConfig

func (app *App) GetConfig[T any](key string) (T, error)

GetConfig decodes the configuration value or sub-tree at the given dotted key path into a value of type T and returns it. It is a convenience wrapper over the application's RawConfig (auto-loaded or supplied via WithRawConfig), saving an explicit app.MustResolve[RawConfig]() plus Unmarshal:

db, err := app.GetConfig[DatabaseConfig]("database")

Like config.(*Config).Get, this is a bootstrap/composition-root helper: read config here and inject typed structs into services via DI rather than reading string keys inside business code (a handler cannot reach this method — there is no App accessor on *Context). A missing key or decode failure returns an error; on error the zero value of T is returned.

func (*App) GetRoute

func (app *App) GetRoute(name string) *Route

GetRoute returns a named route by name.

func (*App) GlobalMiddleware

func (app *App) GlobalMiddleware(middlewares ...Middleware)

GlobalMiddleware appends middleware that runs on every request, including 404 and 405 responses. Must be called before the server starts; panics if called after compile.

func (*App) Group

func (app *App) Group(prefix string) *Group

Group creates a new route group with the given prefix.

func (*App) HEAD

func (app *App) HEAD(pattern string, h Handler) *Route

HEAD registers an explicit HEAD route (overrides auto-generated one). Panics under the same conditions as App.GET.

func (*App) Host

func (app *App) Host(pattern string) *Group

Host creates a route group scoped to the given host pattern. Pattern supports exact labels ("api.example.com"), named parameters ("{tenant}.example.com", "{org:[a-z]+}.platform.io"), and a leftmost anonymous wildcard label ("*.example.com"). Routes registered on the returned Group only match requests whose Host header matches the pattern. Unmatched hosts fall back to the default mux. Returns *Group for API consistency with App.Group.

Host panics if the pattern is a duplicate, overlaps an existing pattern with identical match semantics, contains an invalid wildcard, an invalid regex constraint, or a port. Registering a route on the returned Group panics if a route parameter name collides with a host parameter name. Must be called before the server starts; panics if called after compile.

func (*App) IsDebug

func (app *App) IsDebug() bool

IsDebug reports whether the application is running in debug mode. Debug mode enables development-time warnings (e.g., bind targets that do not implement Validatable). Activated via WithDebug or the server.debug config key.

func (*App) IsRunning

func (app *App) IsRunning() bool

IsRunning reports whether the server is in the running state.

func (*App) Logger

func (app *App) Logger() *slog.Logger

Logger returns the application-level logger used by framework internals. Worker and other integration packages use this accessor to derive framework-scoped loggers without exposing raw logger registration in DI.

func (*App) Mount

func (app *App) Mount(pattern string, handler http.Handler)

Mount attaches another http.Handler as a sub-router under the given pattern. The sub-router receives the remainder of the URL path.

Method scope: the mounted handler is registered for all standard HTTP methods except CONNECT and TRACE, which are excluded deliberately (CONNECT is a proxy mechanism; TRACE enables cross-site tracing). Requests using them receive 405 Method Not Allowed.

Middleware scope: mounted handlers receive only built-in and global middleware. Group-level and route-level middleware do not apply because mounted handlers are plain http.Handler instances dispatched outside the per-route compiled chain. If the mounted sub-application requires authentication or other protections, it must enforce them internally or the protections must be registered as global middleware.

The parent's RouteContext (which may contain internal params like _mount) is stripped before calling the child handler, so the child dispatch creates its own fresh RouteContext. This prevents internal routing state from leaking across mount boundaries.

Must be called before the server starts; panics if called after compile.

func (*App) MustAlias

func (app *App) MustAlias[I, T any]()

MustAlias is like App.Alias but panics on error.

func (*App) MustBindMany

func (app *App) MustBindMany[I, T any]()

MustBindMany is like App.BindMany but panics on error.

func (*App) MustGetConfig

func (app *App) MustGetConfig[T any](key string) T

MustGetConfig is like App.GetConfig but panics on error. It suits composition-root code where a missing or invalid required key should abort startup.

func (*App) MustProvide

func (app *App) MustProvide[T any](constructor any)

MustProvide is like App.Provide but panics on error.

func (*App) MustProvideFactory

func (app *App) MustProvideFactory[T any](fn func(*App) (T, error))

MustProvideFactory is like App.ProvideFactory but panics on error.

func (*App) MustProvideValue

func (app *App) MustProvideValue[T any](value T)

MustProvideValue is like App.ProvideValue but panics on error.

func (*App) MustReplace

func (app *App) MustReplace[T any](value T)

MustReplace is like App.Replace but panics on error.

func (*App) MustResolve

func (app *App) MustResolve[T any]() T

MustResolve is like App.Resolve but panics on error. It is primarily intended for bootstrap/composition-root code.

func (*App) MustResolveAll

func (app *App) MustResolveAll[T any]() []T

MustResolveAll is like App.ResolveAll but panics on error.

func (*App) Mux

func (app *App) Mux() Routes

Mux returns a route registry view for route introspection (Walk, Routes). The returned view includes routes from the default mux and all host-scoped muxes.

func (*App) NewInfra

func (app *App) NewInfra(name string) Infra

NewInfra creates a scoped Infra with the given name. The Logger is tagged with "service"=name and falls back to the framework default logger when the application has none configured.

Use NewInfra for components that live outside the DI container (middleware factories, startup helpers, workers created manually). For DI-managed services, Infra is injected automatically.

func (*App) OPTIONS

func (app *App) OPTIONS(pattern string, h Handler) *Route

OPTIONS registers an OPTIONS route. Panics under the same conditions as App.GET.

func (*App) OnDrain

func (app *App) OnDrain(fn func(ctx context.Context) error)

OnDrain registers a subsystem drain hook that runs after the application lifecycle context is cancelled and before DI infrastructure is shut down. Hooks run concurrently with one another and with HTTP server draining; no ordering between hooks is guaranteed. The ctx carries the same absolute shutdown deadline used by the HTTP drain.

A successful hook must ensure its subsystem can no longer run handlers that depend on application infrastructure. Hooks also run during teardown after an OnStart failure, so they must be idempotent and must not assume their corresponding startup work completed. A hook that ignores ctx may outlive the shutdown budget; Credo reports it as incomplete and proceeds with the expired-context teardown contract rather than claiming graceful success.

Must be called before Run; panics for a nil hook or after the App is frozen.

func (*App) OnPreDrain

func (app *App) OnPreDrain(fn func(ctx context.Context) error)

OnPreDrain registers an early subsystem drain hook that runs after shutdown begins and readiness is withdrawn, but before the application lifecycle context is cancelled. Hooks run concurrently with one another; registration order is identity only, not execution order. The ctx carries the same absolute shutdown deadline used by every later teardown phase.

A successful hook must stop its subsystem's admission and finish work that still depends on live background workers or DI infrastructure. Hooks also run during teardown after an OnStart failure, so they must be idempotent and must tolerate partial startup. If a hook is still running when ctx ends, Credo logs a waiting diagnostic but keeps the lifecycle context and infrastructure live until the hook actually returns. Its completion timestamp determines the final identified incomplete error. Later teardown then continues with the same, possibly expired context rather than racing the pre-drain work.

Most subsystems should use App.OnDrain, which runs after lifecycle cancellation and concurrently with HTTP drain. OnPreDrain is for the narrower case where cancellation itself would tear down a dependency too early.

Must be called before Run; panics for a nil hook or after the App is frozen.

func (*App) OnShutdown

func (app *App) OnShutdown(fn func(ctx context.Context) error)

OnShutdown registers a function to be called during graceful shutdown. Hooks are called in LIFO order (last registered, first called). The ctx passed to each hook carries the shutdown deadline from Shutdown(ctx).

Hooks run on every teardown, including a failed startup (an OnStart hook erroring after an earlier one ran). OnShutdown is the session teardown point, not an OnStart mirror, so hooks must be idempotent and must not assume any particular OnStart hook completed.

Must be called before Run; panics if called after compile.

func (*App) OnStart

func (app *App) OnStart(fn func(ctx context.Context) error)

OnStart registers a function to be called during startup, after the port is bound but before the server starts accepting connections. Hooks are called in FIFO order (first registered, first called). The ctx passed to each hook is the lifecycle context (created at Run time). It is derived independently from any ctx passed to RunContext, so cancelling that ctx during startup does not abort a running hook; the cancellation is observed only after all hooks complete. If any hook returns an error, the server does not start: remaining hooks are skipped, the App runs the full teardown chain (the same as graceful shutdown, including DI shutdown and OnShutdown hooks), and Run returns the error. The App ends terminally stopped — a session that began tears down rather than rolling back, so it cannot be run again (create a new App). Typical uses include cache warm-up. The store/sqldb migration wrapper plugs in directly as app.OnStart(db.Migrate) for development and deliberate single-replica deployments; multi-replica production should use one deadline-bounded pre-deploy migration job instead. Must be called before Run; panics if called after compile.

func (*App) PATCH

func (app *App) PATCH(pattern string, h Handler) *Route

PATCH registers a PATCH route. Panics under the same conditions as App.GET.

func (*App) POST

func (app *App) POST(pattern string, h Handler) *Route

POST registers a POST route. Panics under the same conditions as App.GET.

func (*App) PUT

func (app *App) PUT(pattern string, h Handler) *Route

PUT registers a PUT route. Panics under the same conditions as App.GET.

func (*App) ProtectBinding

func (app *App) ProtectBinding[T any](expected ...T) error

ProtectBinding prevents App.Replace from overwriting the existing direct registration for T. It is idempotent and rejected after Finalize. The method does not resolve or otherwise instantiate T. When one expected value is supplied, protection is a compare-and-protect operation: it succeeds only if the resolved singleton is still that same comparable value.

func (*App) Provide

func (app *App) Provide[T any](constructor any) error

Provide registers a constructor for type T in the application's DI container. The constructor can accept any number of parameters that are themselves registered, and must return T or (T, error).

app.Provide[*UserService](NewUserService)

Because Go cannot express "a function with arbitrary parameters returning T" in the type system, constructor is typed any: signature mistakes (wrong return type, not a function) are reported as an error at registration time, not at compile time. The dependency graph itself is still validated at App.Finalize. For a factory checked entirely by the compiler, see App.ProvideFactory.

func (*App) ProvideFactory

func (app *App) ProvideFactory[T any](fn func(*App) (T, error)) error

ProvideFactory registers a compile-time-checked factory for type T. Unlike App.Provide, whose constructor parameter is typed any and inspected at registration time, fn's signature is enforced by the compiler — and T is inferred from it. fn receives the App and resolves its own dependencies:

app.ProvideFactory(func(app *credo.App) (*UserService, error) {
	repo, err := app.Resolve[*UserRepository]()
	if err != nil {
		return nil, err
	}
	return NewUserService(app.NewInfra("UserService"), repo), nil
})

Like App.Provide, fn runs lazily on first resolution, exactly once, and the instance participates in reverse-order shutdown.

Trade-offs versus App.Provide:

  • fn is opaque to the container. Dependencies resolved inside fn are not part of App.Finalize's graph validation — a missing dependency surfaces at first resolution instead. Cycles entered through fn are likewise invisible to the resolver's cycle detection (the same holds for any constructor closure that captures app and calls App.Resolve).
  • Infra is not auto-injected; use App.NewInfra inside fn as shown.

func (*App) ProvideProtectedValue

func (app *App) ProvideProtectedValue[T any](value T) error

ProvideProtectedValue registers a pre-built singleton whose binding cannot later be overwritten through App.Replace. It is intended for integrations that publish a value together with external lifecycle or health state and therefore cannot safely allow the DI binding to diverge afterward.

func (*App) ProvideValue

func (app *App) ProvideValue[T any](value T) error

ProvideValue registers a pre-built value for type T as a Singleton.

app.ProvideValue[*Logger](logger)

func (*App) RemoveMeta

func (app *App) RemoveMeta(key string)

RemoveMeta removes a root-level metadata key. Must be called before the server starts; panics if called after compile.

func (*App) Replace

func (app *App) Replace[T any](value T) error

Replace registers a pre-built value for type T, overwriting any existing unprotected registration. Unlike App.ProvideValue, a duplicate T is normally replaced along with its cached singleton. Bindings published by App.ProvideProtectedValue or locked through App.ProtectBinding reject replacement because external lifecycle state depends on their identity.

Replace is intended for composition-root overrides and tests where a real binding is swapped for a stub or fake. Because the replacement is a value, it carries no dependencies and stays valid during App.Finalize. Replace is rejected after the container is finalized.

In tests, the github.com/credo-go/credo/testutil package builds on Replace through its WithOverride option.

app.Replace[UserRepo](mockRepo)

func (*App) Resolve

func (app *App) Resolve[T any]() (T, error)

Resolve retrieves an instance of type T from the application's DI container. Resolve is primarily intended for bootstrap/composition-root code; runtime calls remain available, but Credo's recommended application pattern is constructor injection.

svc, err := app.Resolve[*UserService]()

func (*App) ResolveAll

func (app *App) ResolveAll[T any]() ([]T, error)

ResolveAll retrieves all singletons bound to interface type T via App.BindMany, preserving bind order. When no bindings exist, it returns an empty slice and nil error.

func (*App) Routes

func (app *App) Routes() []RouteInfo

Routes returns introspection data for every registered route across the default mux and all host-scoped muxes, plus one entry per App.Mount prefix. See RouteInfo for the per-entry fields, including the route Name, the resolved and shallow-copied Meta, and a mount's forwarded method set.

The result is a deterministic total order — sorted by (Host, Pattern, Method, Kind), independent of registration order and host compile-sort state — so route/permission catalogs and golden-file tests stay stable. The returned slice and each RouteInfo.Meta map are freshly allocated; the caller owns them.

Routes reads live *Route fields (Name, metadata) without locking, so it must not run concurrently with route registration or configuration (Name, SetMeta, Mount). It is a post-wiring (or post-freeze) operation.

func (*App) Run

func (app *App) Run() error

Run starts the HTTP server and blocks until an interrupt (Ctrl+C) or SIGTERM is received, then performs graceful shutdown using the deadline set by WithShutdownTimeout. An App.OnPreDrain hook that ignores that deadline remains a hard teardown barrier and may delay return. A second signal during shutdown force-kills the process. Returns nil on graceful shutdown.

Run serves HTTPS automatically when TLS is configured via WithTLSFiles, WithTLSConfig, or the server.tls.* config keys; otherwise it serves plaintext. A misconfigured certificate (missing file, mismatched pair, or a WithTLSConfig with no certificate source) fails fast before the server accepts connections, rolling the lifecycle back so the App can run again.

Run is the safe default for a process whose lifetime is the server's. For explicit lifecycle control — tests, embedding, or caller-driven cancellation — use App.RunContext.

func (*App) RunContext

func (app *App) RunContext(ctx context.Context) error

RunContext starts the HTTP server and blocks until ctx is cancelled, the server stops, or a programmatic App.Shutdown. Unlike App.Run it installs no signal handler; cancellation is entirely the caller's. On ctx cancellation the drain keeps ctx's values but drops its cancellation and applies the WithShutdownTimeout deadline. An App.OnPreDrain hook that ignores that deadline remains a hard teardown barrier and may delay return. Returns nil on graceful shutdown.

Like App.Run, RunContext serves HTTPS when TLS is configured (via WithTLSFiles, WithTLSConfig, or server.tls.*) and plaintext otherwise, with the same fail-fast certificate validation.

Cancelling ctx during startup does not abort an in-progress App.OnStart hook: hooks receive the lifecycle context, not ctx, so the cancellation takes effect only after all hooks complete.

func (*App) ServeContext

func (app *App) ServeContext(ctx context.Context, l net.Listener) error

ServeContext serves on a caller-provided listener, sharing the same lifecycle as App.RunContext. It is the escape hatch for listeners the framework does not create itself — Unix sockets, a preconfigured test listener, H2C, or an externally managed listener.

ServeContext takes ownership of l: it is closed when the server stops, matching net/http.Server.Serve semantics. Returns nil on graceful shutdown.

ServeContext serves l exactly as given and is TLS-exempt: TLS configured via WithTLSFiles or WithTLSConfig does not apply here, nor does the WithHTTPRedirect listener. For HTTPS on a custom listener, wrap it yourself — e.g. tls.NewListener(l, cfg).

func (*App) ServeHTTP

func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler. It compiles the handler chain on first call using sync.Once for thread safety.

func (*App) SetErrorRenderer

func (app *App) SetErrorRenderer(r ErrorRenderer)

SetErrorRenderer sets the renderer that formats error responses. The framework handles error classification, logging, HEAD handling, and committed-response guards internally; the renderer receives an ErrorInfo containing the original error, the i18n message key, and the classified ProblemDetails. Passing nil restores the default RFC 7807 JSON renderer.

Must be called before the server starts; panics if called after compile.

func (*App) SetMeta

func (app *App) SetMeta(key string, val any)

SetMeta sets a root-level metadata key-value pair. Must be called before the server starts; panics if called after compile.

func (*App) SetSuccessRenderer

func (app *App) SetSuccessRenderer(r SuccessRenderer)

SetSuccessRenderer sets the renderer that formats successful responses sent through Context.Render. It is opt-in: with no renderer installed, Render falls back to plain JSON and the framework imposes no response envelope. The raw Response helpers (Response.JSON and friends) are never routed through it, so an enterprise envelope ({code,message,data}, HAL, JSON:API, …) applies only where handlers opt in via Render. Passing nil restores the default.

Must be called before the server starts; panics if called after compile.

func (*App) Shutdown

func (app *App) Shutdown(ctx context.Context) error

Shutdown gracefully shuts down the server: it withdraws readiness, runs App.OnPreDrain, cancels the lifecycle context, drains in-flight HTTP requests and App.OnDrain subsystem hooks in parallel, tears down DI singletons (reverse order), then runs OnShutdown hooks (LIFO). The caller's ctx carries the shared absolute deadline; WithShutdownTimeout does not replace it. An OnPreDrain hook that ignores ctx remains a hard teardown barrier and may delay return beyond that deadline. Returns an error if the server is not running, or if any shutdown step fails or remains incomplete (joined via errors.Join).

func (*App) State

func (app *App) State() string

State returns the current lifecycle state as a string.

func (*App) Static

func (app *App) Static(prefix string, fsys fs.FS, cfgs ...StaticConfig) *StaticRoute

Static registers routes that serve files from fsys under the given URL prefix. Returns a *StaticRoute for fluent configuration (Name, SetMeta, Middleware).

Internally registers two routes: a catch-all for file paths and an exact match for the prefix itself (serves the index file). Both share the same handler and receive fluent configuration uniformly via StaticRoute.

Panics if prefix contains { or } (route parameters in a static prefix are not meaningful) or if called after compile. The cache presets (StaticCacheMaxAge, StaticCacheImmutableAssets) panic at their own call site on invalid durations.

Production recommendation: use DirFS (or os.Root.FS()) for symlink-safe disk serving. os.DirFS does not prevent symlink-based path traversal.

func (*App) StatusHandler

func (app *App) StatusHandler(code int, h Handler)

StatusHandler sets a custom handler for the given HTTP status code at the root level. Must be called before the server starts; panics if called after compile.

func (*App) UseHealth

func (app *App) UseHealth(cfgs ...HealthConfig)

UseHealth initializes health check endpoints on the application. With no arguments, it registers both /health (liveness) and /ready (readiness).

UseHealth performs no I/O — it only registers in-process state — so misuse panics like every other registration API (contrast App.UseI18n, which reads locale files and therefore returns an error). Panics if called more than once, if called after compile, or if cfg.Group belongs to a different App.

func (*App) UseI18n

func (app *App) UseI18n(cfgs ...I18nConfig) error

UseI18n initializes i18n for the application. It loads locale files, stores the bundle, and adds a global middleware for locale detection.

Behavior:

  • No args or zero-value cfg: reads RawConfig "i18n" key; if absent, uses defaults (dir="locales/", default="en").
  • Dir doesn't exist or is empty: returns nil (i18n inactive, no middleware).
  • Malformed files: returns error.
  • Valid files: loads bundle, adds locale detection middleware.

Unlike registration-only setup APIs such as App.UseHealth, UseI18n reads locale files from disk or an fs.FS — an external operation that can fail for reasons other than a programming mistake — so failures are returned as errors rather than panicking. It still panics if called after compile, like all configuration APIs.

type Context

type Context struct {
	// contains filtered or unexported fields
}

Context is the request-scoped struct that holds the Request, Response, matched Route, logger, and a key-value store.

Context is pooled via sync.Pool for zero-allocation request handling. Access fields only through methods (not direct field access) to ensure pool safety. Because it is pooled, Context deliberately does not implement context.Context; call Context.Context to obtain the request's context.Context for context-taking APIs.

Context is Credo's HTTP request context. Lifecycle hooks such as App.OnStart and App.OnShutdown receive a standard context.Context, not *Context.

func NewContext

func NewContext(w http.ResponseWriter, r *http.Request) *Context

NewContext creates a new Context wrapping the given ResponseWriter and Request. This is primarily useful for testing error handlers and middleware.

func (*Context) AddLogAttrs

func (c *Context) AddLogAttrs(args ...any)

AddLogAttrs adds attributes to the request-scoped logger for the remainder of this request. The new logger is derived from Context.Logger, so enrichment added earlier — such as the request_id attribute — is preserved by construction. args are key-value pairs as accepted by slog.Logger.With:

ctx.AddLogAttrs("tenant_id", tenantID)

Prefer this over Context.SetLogger when the goal is to add attributes rather than replace the logger. Calling it with no arguments is a no-op.

func (*Context) Context

func (c *Context) Context() context.Context

Context returns the underlying request's context.Context. Use it for APIs that take a context.Context — database queries, downstream requests, or transaction propagation through a data-store API:

err := db.InTx(ctx.Context(), func(txCtx context.Context) error {
	return service.Update(txCtx)
})

The returned context is canceled when the request completes. For background work that must outlive the request, detach it with context.WithoutCancel:

bg := context.WithoutCancel(ctx.Context())
go process(bg)

Context deliberately does NOT implement context.Context itself: it is pooled (sync.Pool) and reused across requests, so retaining a *Context as a long-lived context.Context would observe a later request's state. Context hands back the real, non-pooled request context instead.

func (*Context) Get

func (c *Context) Get(key string) any

Get retrieves a value from the request-scoped store.

func (*Context) GetUser

func (c *Context) GetUser[T any]() (T, bool)

GetUser returns the authenticated user previously stored under type T and reports whether one was present. The type argument is required because it cannot be inferred from a return value: ctx.GetUser[*User](). Retrieve with the same T that was stored — a value set under a concrete type is not visible through an interface type parameter.

func (*Context) HasRequestLogger

func (c *Context) HasRequestLogger() bool

HasRequestLogger reports whether a request-scoped logger has been set for this request — by the built-in request ID tier, middleware.RequestID, Context.SetLogger, or Context.AddLogAttrs. It does not inspect the logger's attributes.

The framework's log emitters (access log, panic recovery) use it as a convention-based signal: under the derivation contract documented on Context.SetLogger, a request-scoped logger is assumed to already carry request_id, so the emitters skip adding the attribute explicitly.

func (*Context) HasRoute

func (c *Context) HasRoute() bool

HasRoute reports whether a route matched for this request. Returns false inside custom 404/405 status handlers where no route exists. Use this to guard Context.Route calls in middleware that may run on unmatched-request paths:

if ctx.HasRoute() {
    val, _ := ctx.Route().LookupMeta("permission")
}

func (*Context) IsRewriting

func (c *Context) IsRewriting() bool

IsRewriting reports whether this context has a pending internal rewrite. This can be useful in middleware to avoid side-effects (e.g., response writing) when the handler signaled a re-dispatch.

func (*Context) Locale

func (c *Context) Locale() string

Locale returns the detected locale string for this request (e.g., "en", "tr"). Returns an empty string if i18n is not configured.

func (*Context) Logger

func (c *Context) Logger() *slog.Logger

Logger returns the request-scoped logger. Falls back through the chain: request logger → app logger → nop logger.

func (*Context) OriginalPath

func (c *Context) OriginalPath() string

OriginalPath returns the request path as received from the client, before any rewriting (middleware.Rewrite or ctx.Rewrite). Useful for access logging, analytics, and debugging.

func (*Context) Render

func (c *Context) Render(status int, data any) error

Render sends a successful response through the app's SuccessRenderer when one is installed via App.SetSuccessRenderer, letting an application apply a uniform response envelope at a single seam. With no renderer installed (the default), it falls back to plain JSON via Response.JSON and imposes no envelope.

Render is the only success path that consults the renderer: the raw helpers (Response.JSON, Response.XML, Response.Text, Response.Blob, and the streaming writers) stay un-intercepted, so handlers serving webhooks, health probes, or third-party-dictated shapes can always bypass the envelope by calling them directly. A renderer error propagates to the caller (and thus the error pipeline) like any handler error.

func (*Context) Request

func (c *Context) Request() *Request

Request returns the Request for this context.

func (*Context) RequestID

func (c *Context) RequestID() string

RequestID returns the current request ID. It returns an empty string when request ID middleware is not active.

func (*Context) RequireUser

func (c *Context) RequireUser[T any]() (T, error)

RequireUser is like Context.GetUser but returns a handler-ready error when the user is absent: ErrUnauthorized wrapping ErrUserMissing. The framework renders it as 401, and errors.Is(err, ErrUserMissing) still reports the cause. The type argument is required: ctx.RequireUser[*User]().

func (*Context) Response

func (c *Context) Response() *Response

Response returns the Response for this context.

func (*Context) Rewrite

func (c *Context) Rewrite(path string) error

Rewrite triggers an internal re-dispatch to the given path. The client is unaware of the rewrite (no HTTP redirect is sent). The original request path is preserved in OriginalPath(). The matched host scope does not change.

Rewrite must be the last call in a handler — the return value must be returned directly:

return ctx.Rewrite("/new-path")

A maximum of 10 rewrites per request is enforced to prevent loops. If exceeded, an error is returned and the request fails with 500.

func (*Context) Route

func (c *Context) Route() *Route

Route returns the matched Route (for accessing Meta, Name, BuildURI). Returns nil when no route matched (e.g., inside custom 404/405 handlers). Use Context.HasRoute to guard against nil before calling Route methods.

func (*Context) Set

func (c *Context) Set(key string, val any)

Set stores a key-value pair in the request-scoped store.

func (*Context) SetLogger

func (c *Context) SetLogger(l *slog.Logger)

SetLogger replaces the request-scoped logger for the remainder of this request. It is the wholesale-replacement API: reach for it when the logger itself must change (different handler, level, or destination). To add request-bound attributes, prefer Context.AddLogAttrs.

When replacing, derive the new logger from Context.Logger so existing enrichment is preserved:

ctx.SetLogger(ctx.Logger().With("tenant_id", tenantID))

A logger built from scratch silently drops attributes added earlier — including the request_id added by the request ID tier — from all subsequent log output (handler logs and the access log). The framework cannot detect this; if you must start from a fresh logger, re-attach the ID via Context.RequestID.

The logger is cleared automatically when the request completes.

func (*Context) SetUser

func (c *Context) SetUser[T any](user T)

SetUser stores the authenticated user on the request, keyed by its type T. It is the framework's blessed way to attach a principal: T is inferred from the argument, so call sites read ctx.SetUser(user) with no explicit type argument. Storing a different type adds a separate slot; storing the same type again replaces the previous value.

github.com/credo-go/credo/auth.Middleware calls this after a successful Authenticate; custom middleware may call it directly.

func (*Context) T

func (c *Context) T(key string, data ...map[string]any) string

T translates a message key using the detected locale. If i18n is not configured or the key is not found, the key itself is returned. Optional data map provides template variables for the message.

T always renders the message's Other plural form; for count-based plural selection use Context.TPlural.

func (*Context) TPlural

func (c *Context) TPlural(key string, count any, data ...map[string]any) string

TPlural translates a message key using the detected locale, rendering the CLDR cardinal plural form selected for count. count may be any integer kind, an integral float, or a decimal string ("1.5") when visible fraction digits matter. The count value is exposed to the template as {{.count}}.

// messages.json: "items": {"one": "{{.count}} item", "other": "{{.count}} items"}
ctx.TPlural("items", 1) // "1 item"
ctx.TPlural("items", 5) // "5 items"

If i18n is not configured or the key is not found, the key itself is returned. When count cannot be interpreted as a number, the Other form is rendered.

type ErrorInfo

type ErrorInfo struct {
	// Err is the original error returned by the handler.
	// Use [errors.As] / [errors.Is] for type-specific behavior
	// (e.g., Sentry reporting, extracting metadata for custom headers).
	Err error

	// MessageKey is the i18n message key used to resolve [ProblemDetails.Title].
	// This is the raw key before resolution (e.g., "http.not_found",
	// "user.email_exists"). Useful for client-side i18n, telemetry
	// grouping, or custom error code mapping.
	MessageKey string

	// Problem is the framework-classified RFC 7807 Problem Details.
	// The renderer may use it as-is, modify it, or ignore it entirely
	// and write a custom response format.
	Problem *ProblemDetails
}

ErrorInfo carries the original error and the framework-classified ProblemDetails to the ErrorRenderer.

type ErrorRenderer

type ErrorRenderer func(ctx *Context, info ErrorInfo)

ErrorRenderer formats an error response given a classified ErrorInfo. The framework handles error classification, logging, and committed-response guards internally. ErrorRenderer is called for all HTTP methods including HEAD, allowing it to set response headers (e.g., Retry-After, WWW-Authenticate). For HEAD requests where the renderer does not commit the response, the framework sends a status-only response (no body).

If ErrorRenderer does not write a response (Response.Committed remains false), the framework sends a status-only response for HEAD, or falls back to the default RFC 7807 JSON renderer for other methods.

Register a custom renderer with App.SetErrorRenderer.

type Group

type Group struct {
	// contains filtered or unexported fields
}

Group is the user-facing route group for configuring routes with a shared prefix, middleware, and metadata. It is also the node of the group parent chain: route metadata (LookupMeta) and group middleware are both resolved by walking this chain — metadata dynamically at request time, middleware once at compile time.

func (*Group) DELETE

func (g *Group) DELETE(pattern string, h Handler) *Route

DELETE registers a DELETE route in this group. Panics under the same conditions as App.GET.

func (*Group) File

func (g *Group) File(urlPath string, fsys fs.FS, name string, cfgs ...StaticConfig) *Route

File registers a single GET route that serves one named file from fsys within this group. See App.File for full documentation.

func (*Group) GET

func (g *Group) GET(pattern string, h Handler) *Route

GET registers a GET route in this group. A matching HEAD handler is registered automatically; subsequent calls to Route.Middleware and Route.SetMeta on the returned route apply to HEAD as well, so HEAD requests can never silently bypass auth, rate limiting, or meta-driven middleware. Panics under the same conditions as App.GET.

func (*Group) Group

func (g *Group) Group(prefix string) *Group

Group creates a nested sub-group with the given prefix. The sub-group inherits this group's metadata and middleware through the parent chain.

func (*Group) HEAD

func (g *Group) HEAD(pattern string, h Handler) *Route

HEAD registers a HEAD route in this group. Panics under the same conditions as App.GET.

func (*Group) Middleware

func (g *Group) Middleware(middlewares ...Middleware) *Group

Middleware appends group-level middlewares. They apply to every route of this group and its sub-groups, regardless of whether the route was registered before or after this call: per-route chains are assembled from the group parent chain when the app compiles (first request or Run), mirroring how route metadata is resolved via Route.LookupMeta. Must be called before the server starts; panics if called after compile. Returns *Group for chaining.

func (*Group) OPTIONS

func (g *Group) OPTIONS(pattern string, h Handler) *Route

OPTIONS registers an OPTIONS route in this group. Panics under the same conditions as App.GET.

func (*Group) PATCH

func (g *Group) PATCH(pattern string, h Handler) *Route

PATCH registers a PATCH route in this group. Panics under the same conditions as App.GET.

func (*Group) POST

func (g *Group) POST(pattern string, h Handler) *Route

POST registers a POST route in this group. Panics under the same conditions as App.GET.

func (*Group) PUT

func (g *Group) PUT(pattern string, h Handler) *Route

PUT registers a PUT route in this group. Panics under the same conditions as App.GET.

func (*Group) RemoveMeta

func (g *Group) RemoveMeta(key string)

RemoveMeta removes a metadata key from this group. Must be called before the server starts; panics if called after compile.

func (*Group) SetMeta

func (g *Group) SetMeta(key string, val any)

SetMeta sets a metadata key-value pair for this group. All routes within the group inherit this metadata. Must be called before the server starts; panics if called after compile.

func (*Group) Static

func (g *Group) Static(prefix string, fsys fs.FS, cfgs ...StaticConfig) *StaticRoute

Static registers routes that serve files from fsys under the given URL prefix within this group. See App.Static for full documentation.

type HTTPError

type HTTPError struct {
	// Code is the HTTP status code.
	Code int `json:"code"`

	// MessageKey is the i18n message key or literal fallback message.
	MessageKey string `json:"message_key"`

	// Internal is the underlying error (not exposed to the client).
	Internal error `json:"-"`
}

HTTPError represents an HTTP error with a status code and a message key. The MessageKey field serves as both the i18n translation key and the fallback message when no translation is found.

Resolution order for MessageKey (applied in the error handling pipeline):

  1. i18n bundle lookup — if a translation exists for MessageKey, use it
  2. builtInMessages lookup — if MessageKey matches a built-in key, use it
  3. MessageKey itself — used as-is (works for literal messages)

func NewHTTPError

func NewHTTPError(code int, messageKey ...string) *HTTPError

NewHTTPError creates a new HTTPError with the given status code and optional message key. If no message key is provided, the corresponding MsgKey constant is used (falling back to http.StatusText for unknown codes).

func (*HTTPError) Error

func (e *HTTPError) Error() string

Error implements the error interface.

func (*HTTPError) HTTPStatus

func (e *HTTPError) HTTPStatus() int

HTTPStatus returns the HTTP status code carried by the error.

func (*HTTPError) Unwrap

func (e *HTTPError) Unwrap() error

Unwrap returns the internal error, supporting errors.Is/As.

func (*HTTPError) WithInternal

func (e *HTTPError) WithInternal(err error) *HTTPError

WithInternal returns a copy of the error with the internal error set.

type Handler

type Handler func(ctx *Context) error

Handler is the Credo handler signature. Handlers return an error for centralized error handling via the App's error handling pipeline.

type HealthCheckFunc

type HealthCheckFunc func(ctx context.Context) error

HealthCheckFunc adapts a plain function to HealthChecker.

func (HealthCheckFunc) Check

func (f HealthCheckFunc) Check(ctx context.Context) error

Check implements HealthChecker.

type HealthChecker

type HealthChecker interface {
	Check(ctx context.Context) error // nil = healthy
}

HealthChecker checks the health of a component.

type HealthConfig

type HealthConfig struct {
	// Enabled controls whether health endpoints are registered.
	// nil (default) = true.
	Enabled *bool

	// Liveness controls whether the liveness endpoint is registered.
	// nil (default) = true.
	Liveness *bool

	// Readiness controls whether the readiness endpoint is registered.
	// nil (default) = true.
	Readiness *bool

	// LivenessPath is the path for the liveness endpoint. Default: "/health".
	LivenessPath string

	// ReadinessPath is the path for the readiness endpoint. Default: "/ready".
	ReadinessPath string

	// CheckTimeout is the enforced per-check timeout. Default: 5s. A callback
	// that ignores cancellation cannot delay the response beyond this budget;
	// overlapping requests share its still-running execution until it exits.
	CheckTimeout time.Duration

	// ExposeErrors includes check error strings in the readiness response
	// body. Default false: failing checks report only "down" and the error
	// is written to the application log instead — error strings often
	// carry internal details (hostnames, connection targets) that should
	// not reach unauthenticated probe endpoints.
	ExposeErrors bool

	// Group registers health routes on a specific route group instead of the
	// app root. Routes inherit the group's prefix and middleware chain.
	// nil (default) = routes are registered on the app root.
	Group *Group

	// LogRequests includes health and readiness probe requests in the access
	// log. Default false: probe traffic (such as Kubernetes liveness and
	// readiness checks) is silenced via the MetaAccessLog route meta to keep
	// logs clean. The endpoints remain registered and responsive either way.
	//
	// The meta is set at the route level, so LogRequests:true re-enables
	// logging even when the health routes sit under a group that silenced
	// access logging (a route-level value overrides its group's).
	LogRequests bool
}

HealthConfig configures the health check endpoints.

type I18nConfig

type I18nConfig struct {
	// Dir is the filesystem path to the locale directory (e.g., "locales/").
	// Mutually exclusive with DirFS.
	Dir string

	// DirFS is an embed.FS or any fs.FS providing locale files.
	// Mutually exclusive with Dir.
	DirFS fs.FS

	// Default is the default language tag string (e.g., "en").
	// Falls back to "en" if empty.
	Default string

	// Detect is a function that extracts the preferred language from an HTTP request.
	// Defaults to reading the Accept-Language header if nil.
	Detect func(r *http.Request) string
}

I18nConfig configures internationalization for the application.

type Infra

type Infra struct {

	// Logger is a structured logger, scoped to the service.
	Logger *slog.Logger
	// contains filtered or unexported fields
}

Infra carries framework infrastructure to services. The container produces it automatically when detected as a constructor parameter (Model 1).

Logger is scoped per service: each service receives a logger with a "service" attribute set to the service type name.

When no logger is configured via WithLogger, Infra falls back to a default stderr logger. Future infrastructure (metrics, tracing) will be added as new fields by the observability release.

For testing, construct Infra directly:

infra := credo.Infra{Logger: slog.New(slog.NewTextHandler(os.Stderr, nil))}

type Middleware

type Middleware func(next Handler) Handler

Middleware is the single middleware type used throughout Credo. All middleware — global, group, and route level — uses this signature.

func WrapStdMiddleware

func WrapStdMiddleware(m StdMiddleware) Middleware

WrapStdMiddleware converts stdlib middleware (func(http.Handler) http.Handler) into a Credo Middleware (func(Handler) Handler). This allows using any existing Go community middleware with Credo's unified middleware stack.

app.GlobalMiddleware(credo.WrapStdMiddleware(corsMiddleware))

type Option

type Option func(*appOptions)

Option configures the App during construction.

func WithAccessLogSkipper

func WithAccessLogSkipper(skip func(*Context) bool) Option

WithAccessLogSkipper installs a predicate consulted by the built-in access logger; when it returns true the request is not logged. Use it to silence noisy paths (metrics scrape, static assets) without disabling the logger entirely. For per-route or per-group silencing prefer the MetaAccessLog route meta, and note that health probes are already silenced by default (see HealthConfig.LogRequests).

The predicate runs BEFORE routing, so only request-level data is reliable (method, path, and headers via ctx.Request()); ctx.Route(), route params, and the response status are not yet set. For route-based decisions use MetaAccessLog; status-based filtering is a separate, deferred concern.

This has no effect when the built-in access logger is disabled via WithoutAccessLog; the configurable [middleware.AccessLog] has its own Skipper field.

func WithAddr

func WithAddr(host string, port int) Option

WithAddr sets the listen address directly (for testing or programmatic use).

func WithDebug

func WithDebug() Option

WithDebug enables development-mode warnings. When active, the framework logs warnings for common mistakes such as binding a struct that does not implement validation.Validatable. Can also be enabled via the server.debug config key.

func WithHTTPRedirect

func WithHTTPRedirect(addr string) Option

WithHTTPRedirect runs a second, plaintext listener on addr (for example ":80") whose only job is to permanently redirect every request to its HTTPS equivalent. GET and HEAD receive 301; other methods receive 308 so the method (and body) are preserved — matching the framework's trailing-slash redirect convention. The redirect target reuses the request host with the TLS server's port (omitted when it is 443).

TLS must be configured (via WithTLSFiles, WithTLSConfig, or server.tls.*); otherwise startup fails fast at preflight, since redirecting to an HTTPS server that does not exist makes no sense. The redirect listener starts and drains with the main server, and a runtime failure of the redirect listener tears the whole app down — the same as a failure of the main listener — so a requested redirect can never silently die while the app reports healthy. It does not apply to ServeContext, which serves the caller's listener as-is.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the application-level logger. Each service receives a scoped copy with a "service" attribute. If not set, the framework default logger (a text handler writing to stderr) is used, so access and request logging are on by default without any configuration.

func WithMaxBodyBytes

func WithMaxBodyBytes(n int64) Option

WithMaxBodyBytes sets the maximum number of bytes read from any request body. Requests whose body exceeds the limit receive 413 Request Entity Too Large. A negative value disables the limit; zero (the default) applies a 4 MiB cap.

func WithRawConfig

func WithRawConfig(rc RawConfig) Option

WithRawConfig sets the RawConfig for the application. When provided, New does not auto-load configuration from files, .env, or environment variables; the given RawConfig is registered in DI as-is. The framework still reads its internal server settings from the "server" key when present.

Use this option when config has already been loaded explicitly, for example via config.Load(config.WithFiles(...)) or config.LoadBytes(...).

func WithRedirectTrailingSlash

func WithRedirectTrailingSlash(enabled bool) Option

WithRedirectTrailingSlash controls whether the router automatically redirects requests whose trailing slash variant matches a registered route. GET/HEAD requests receive 301; other methods receive 308 (preserving the method). Defaults to true when not set.

func WithShutdownTimeout

func WithShutdownTimeout(d time.Duration) Option

WithShutdownTimeout sets the graceful-shutdown drain budget used by the signal-aware Run and by context-cancellation-triggered RunContext. The parallel HTTP/OnDrain phase, DI singleton cleanup, and OnShutdown hooks must complete within this single absolute budget. Zero (the default) applies a 30s budget. An explicit Shutdown(ctx) call ignores this and honours the caller's context deadline instead. Can also be set via the server.shutdown_timeout config key.

func WithTLSConfig

func WithTLSConfig(cfg *tls.Config) Option

WithTLSConfig configures HTTPS from a fully-formed *tls.Config, exposing the complete crypto/tls surface: mutual TLS, SNI via GetCertificate, custom minimum version and cipher suites, ALPN, and hot certificate reload. It has the highest TLS precedence — when set, WithTLSFiles and the server.tls.* keys are ignored.

The config must carry a certificate source (Certificates, GetCertificate, or GetConfigForClient), validated at startup. It is cloned before use, so the framework never mutates the caller's value and later caller mutations do not affect the running server. Passing a nil config is a configuration error caught at startup: an explicit WithTLSConfig(nil) does not silently fall back to WithTLSFiles, the config keys, or plaintext.

func WithTLSFiles

func WithTLSFiles(certFile, keyFile string) Option

WithTLSFiles configures HTTPS by loading the certificate and private key from the given PEM file paths. When set, Run and RunContext serve TLS; the key pair is loaded and validated at startup (before the server accepts connections), so a missing file or mismatched pair fails fast. The same paths may instead come from the server.tls.cert_file / server.tls.key_file config keys — WithTLSFiles takes precedence over those. It is in turn shadowed by WithTLSConfig.

Calling it with an empty cert or key path is a configuration error caught at startup: an explicit but empty WithTLSFiles does not silently fall back to the config keys or to plaintext. For conditional TLS, omit the option entirely rather than passing empty strings.

This option performs no I/O; the files are read when the server starts.

func WithTrustedProxies

func WithTrustedProxies(cidrs ...string) Option

WithTrustedProxies configures the CIDR ranges from which forwarded headers such as X-Forwarded-For, X-Forwarded-Proto, and X-Real-IP are trusted. Requests whose immediate peer is outside this list ignore forwarded headers.

Pass no entries (the default) to disable proxy-header trust entirely. Invalid CIDR entries cause New to return an error.

func WithoutAccessLog

func WithoutAccessLog() Option

WithoutAccessLog disables the built-in access logger. By default, every request is logged with method, path, status, bytes, duration, remote_addr (from Request.RealIP), and user_agent attributes.

Disable this if you use [middleware.AccessLog] with custom configuration (e.g., a Skipper function). Using both the built-in and middleware loggers produces duplicate log entries.

func WithoutRecover

func WithoutRecover() Option

WithoutRecover disables the built-in panic recovery that wraps the entire handler chain. By default, Credo recovers from panics in all middleware and handlers, logs the stack trace, and returns 500 Internal Server Error.

Disable this if you provide your own recovery mechanism or need panics to propagate (e.g., in tests).

func WithoutRequestID

func WithoutRequestID() Option

WithoutRequestID disables the built-in request ID middleware. By default, every request gets a unique ID (set on context and X-Request-Id header), and the request-scoped logger is enriched with the request_id attribute.

Disable this if you use [middleware.RequestID] with custom configuration (e.g., different header name, custom generator). Note that the built-in access logger will still work but request_id will not appear in logs unless the custom middleware also enriches ctx.Logger().

type ProblemDetails

type ProblemDetails struct {
	// Type is a URI reference that identifies the problem type.
	// Defaults to "about:blank" per RFC 7807.
	Type string `json:"type"`

	// Title is a short, human-readable summary of the problem type.
	Title string `json:"title"`

	// Status is the HTTP status code.
	Status int `json:"status"`

	// Detail is a human-readable explanation specific to this occurrence.
	Detail string `json:"detail,omitempty"`

	// Instance is a URI reference that identifies the specific occurrence.
	Instance string `json:"instance,omitempty"`

	// Errors holds field-level validation errors (if any).
	Errors []validation.ValidationError `json:"errors,omitempty"`
}

ProblemDetails represents an RFC 7807 Problem Details response.

func NewProblemDetails

func NewProblemDetails(status int, title string) *ProblemDetails

NewProblemDetails creates a new ProblemDetails with the given status and title. Type defaults to "about:blank" per RFC 7807.

type RawConfig

type RawConfig = config.RawConfig

RawConfig is an alias for config.RawConfig. The interface is defined in the config package to avoid circular imports between the root package and the config package. This alias ensures that credo.RawConfig continues to work seamlessly throughout the framework.

RawConfig provides low-level access to the merged configuration. This is a bootstrap mechanism — application code should use typed config structs injected via DI instead of calling RawConfig directly.

Unmarshal decodes both struct sections and primitive values:

var port int
rawCfg.Unmarshal("server.port", &port)

var dbCfg DatabaseConfig
rawCfg.Unmarshal("databases.default", &dbCfg)

type Request

type Request struct {
	*http.Request
	// contains filtered or unexported fields
}

Request wraps *http.Request and provides request-side helpers: route parameters, query parameter shortcuts, and body/query binding.

func NewRequest

func NewRequest(r *http.Request) *Request

NewRequest creates a new Request wrapping the given *http.Request.

func (*Request) BindBody

func (r *Request) BindBody(target any) error

BindBody decodes the request body into target based on the Content-Type header. Supported content types:

  • application/json (default when Content-Type is absent)
  • application/xml, text/xml
  • application/x-www-form-urlencoded (uses "form" struct tags)
  • multipart/form-data (uses "form" struct tags, including file fields)

If target implements validation.Validatable, Validate() is called automatically after successful decoding ("parse, don't validate").

Returns 400 Bad Request for empty body or decode errors, 415 Unsupported Media Type for unrecognized content types.

func (*Request) BindQuery

func (r *Request) BindQuery(target any) error

BindQuery decodes URL query parameters into target using `query:"name"` struct tags. If target implements validation.Validatable, Validate() is called automatically after successful decoding ("parse, don't validate").

In debug mode, a warning is logged when the target does not implement Validatable.

func (*Request) PathValue

func (r *Request) PathValue(name string) string

PathValue returns the route parameter for name, falling back to the embedded request's http.Request.PathValue. Prefer Request.RouteParam in new code.

This shadow exists for stdlib muscle memory: Credo's dispatcher does not populate the embedded *http.Request's path values (doing so would cost an allocation per request for data Request.RouteParam already serves), so without it ctx.Request().PathValue("id") would silently return "". The raw embedded request — as seen by stdlib handlers via App.Mount or middleware via WrapStdMiddleware — still carries no path values.

func (*Request) QueryParam

func (r *Request) QueryParam(name string) string

QueryParam returns a query string parameter value by name. Returns "" if not present.

func (*Request) RealIP

func (r *Request) RealIP() string

RealIP returns the address of the original client.

When the immediate peer RemoteAddr is trusted, RealIP walks the proxy chain — the RFC 7239 Forwarded header's for= parameters first, then X-Forwarded-For — from right to left, skipping trusted proxy hops and returning the first untrusted address. If neither yields a usable value, X-Real-IP is used. If the peer is untrusted, all forwarded headers are ignored.

The returned value is an IP address only for parseable addresses. If RemoteAddr itself is unparseable, RealIP falls back to RemoteAddr verbatim.

func (*Request) RouteParam

func (r *Request) RouteParam(name string) string

RouteParam returns the URL parameter value for name — a path parameter such as {id}, or, for host-scoped routes, a host parameter. It returns "" when the parameter is not present.

RouteParam is the preferred accessor for single values; it never allocates:

id := ctx.Request().RouteParam("id")

Unlike the map returned by Request.RouteParams, the returned string is safe to retain after the request completes.

func (*Request) RouteParams

func (r *Request) RouteParams() map[string]string

RouteParams returns all URL parameter key-value pairs. For host-scoped routes, host params and path params share the same namespace.

The map is a read-only view, materialized lazily on first call: writes to it are not seen by Request.RouteParam. It is owned by the framework and recycled after the request completes — do not retain it or read it from another goroutine after the handler returns. For single values, prefer Request.RouteParam.

func (*Request) Scheme

func (r *Request) Scheme() string

Scheme reports the scheme the original client used: "http" or "https".

If the request arrived over TLS directly, Scheme returns "https". Otherwise, forwarded scheme headers are considered only when the immediate peer RemoteAddr is configured as a trusted proxy on the App. Untrusted peers cannot influence the result.

Only "http" and "https" are returned. Invalid forwarded header values fall back to the underlying transport.

type Response

type Response struct {
	http.ResponseWriter
	// contains filtered or unexported fields
}

Response wraps http.ResponseWriter to track the response status code, byte count, and whether headers have been committed.

The embedded ResponseWriter may be swapped by middleware that wraps the writer (e.g., compression); the tracking state is framework-owned and exposed read-only via Response.Status, Response.Size, and Response.Committed, and Response.Hijacked.

func NewResponse

func NewResponse(w http.ResponseWriter) *Response

NewResponse creates a new Response wrapping the given http.ResponseWriter.

func (*Response) Blob

func (r *Response) Blob(code int, contentType string, b []byte) error

Blob sends a binary response with the given content type.

func (*Response) Committed

func (r *Response) Committed() bool

Committed reports whether the response header has been written. Once committed, the status code and headers can no longer change.

func (*Response) Flush

func (r *Response) Flush()

Flush sends any buffered data to the client.

func (*Response) HTML

func (r *Response) HTML(code int, html string) error

HTML sends an HTML response with the given status code.

func (*Response) Hijack

func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error)

Hijack implements the http.Hijacker interface. It resolves nested Unwrap chains and marks the response only after the effective Hijack call succeeds.

func (*Response) Hijacked

func (r *Response) Hijacked() bool

Hijacked reports whether the underlying HTTP connection was successfully taken over through Response.Hijack. Writing status or Upgrade headers alone does not mark the response as hijacked. Middleware that bypasses Response and hijacks a raw underlying writer cannot be observed by this state.

func (*Response) JSON

func (r *Response) JSON(code int, v any) error

JSON sends a JSON response with the given status code.

func (*Response) NoContent

func (r *Response) NoContent(code int) error

NoContent sends a response with no body.

func (*Response) Redirect

func (r *Response) Redirect(code int, url string) error

Redirect sends an HTTP redirect response.

func (*Response) Reset

func (r *Response) Reset(w http.ResponseWriter)

Reset resets the response for reuse from the pool.

func (*Response) SetCookie

func (r *Response) SetCookie(cookie *http.Cookie)

SetCookie adds a Set-Cookie header to the response.

func (*Response) Size

func (r *Response) Size() int64

Size returns the number of bytes written to the response body.

func (*Response) Status

func (r *Response) Status() int

Status returns the HTTP status code written, or 0 when the response has not been committed yet.

func (*Response) Stream

func (r *Response) Stream(code int, contentType string, rd io.Reader) error

Stream sends a streaming response from the given reader.

func (*Response) String

func (r *Response) String() string

String implements fmt.Stringer for debugging.

func (*Response) Text

func (r *Response) Text(code int, s string) error

Text sends a plain text response with the given status code. Named Text (not String) to avoid conflict with the fmt.Stringer interface.

func (*Response) Unwrap

func (r *Response) Unwrap() http.ResponseWriter

Unwrap returns the underlying http.ResponseWriter.

func (*Response) Write

func (r *Response) Write(b []byte) (int, error)

Write writes the data to the connection as part of an HTTP reply. If WriteHeader has not been called, it calls WriteHeader(200).

func (*Response) WriteHeader

func (r *Response) WriteHeader(code int)

WriteHeader sends an HTTP response header with the given status code. It can only be called once per response.

func (*Response) XML

func (r *Response) XML(code int, v any) error

XML sends an XML response with the given status code.

type Route

type Route struct {
	// contains filtered or unexported fields
}

Route represents a registered route with its handler, metadata, and fluent configuration methods.

func (*Route) BuildURI

func (r *Route) BuildURI(params ...string) (string, error)

BuildURI generates a URI from the route pattern by replacing named parameters with the provided values in order. It returns an error when a parameter is missing, when too many values are provided, or when the route pattern is malformed. Uses brace-depth-aware parsing to correctly handle regex quantifiers like {id:[0-9]{2,4}}.

uri, err := route.BuildURI("42") // "/users/42"

func (*Route) BuildURL

func (r *Route) BuildURL(params ...string) (string, error)

BuildURL generates a full URL by combining the route's host pattern and path pattern, replacing parameters with the provided values in order. Host pattern parameters are consumed first, then path parameters. It returns an error when a parameter is missing, when too many values are provided, or when either pattern is malformed. Wildcard host patterns cannot generate concrete URLs and return an error.

For host-scoped routes:

// Host: {tenant}.myapp.com, Path: /users/{id}
url, err := route.BuildURL("acme", "42") // "acme.myapp.com/users/42"

For default (non-host-scoped) routes, BuildURL is equivalent to Route.BuildURI:

url, err := route.BuildURL("42") // "/users/42"

func (*Route) GetHost

func (r *Route) GetHost() string

GetHost returns the host pattern for host-scoped routes. Returns an empty string for routes on the default mux.

func (*Route) GetMethod

func (r *Route) GetMethod() string

GetMethod returns the HTTP method.

func (*Route) GetName

func (r *Route) GetName() string

GetName returns the route name.

func (*Route) GetPattern

func (r *Route) GetPattern() string

GetPattern returns the URL pattern.

func (*Route) LookupMeta

func (r *Route) LookupMeta(key string) (any, bool)

LookupMeta searches for a metadata value by key, traversing the parent chain (route → group → parent group → ...) until found.

func (*Route) Middleware

func (r *Route) Middleware(m ...Middleware) *Route

Middleware appends one or more per-route middlewares. Must be called before the server starts; panics if called after compile. Returns the Route for fluent chaining.

For GET routes with an auto-generated HEAD twin, middleware is also appended to the twin so that HEAD requests run the same chain as GET — otherwise auth, rate limiting, and other middleware could be silently bypassed via HEAD.

func (*Route) Name

func (r *Route) Name(name string) *Route

Name sets the route name for URL generation. Route names must be unique within the App. Duplicate names panic. Must be called before the server starts; panics if called after compile. Returns the Route for fluent chaining.

func (*Route) SetMeta

func (r *Route) SetMeta(key string, val any) *Route

SetMeta attaches a key-value metadata pair to this route. Must be called before the server starts; panics if called after compile. Returns the Route for fluent chaining.

For GET routes with an auto-generated HEAD twin, the value is also set on the twin so middleware reading meta sees identical values regardless of which method was used.

type RouteContext

type RouteContext = radix.RouteContext

RouteContext is the routing context type used by the radix tree. Re-exported from internal/radix so consumers can reference this type without importing internal packages.

type RouteInfo

type RouteInfo struct {
	// Method is the HTTP method of a normal route; empty for a mount (see Methods).
	Method string

	// Methods is the sorted set of forwarded HTTP methods for a mount
	// (CONNECT and TRACE excluded); nil for a normal route.
	Methods []string

	// Pattern is the user-facing URL pattern; for a mount it is the cleaned
	// mount prefix.
	Pattern string

	// Host is the host pattern, or empty for the default mux.
	Host string

	// Name is the route name, or empty if unnamed.
	Name string

	// Meta is the resolved, shallow-copied route metadata; nil if none.
	Meta map[string]any

	// Kind reports whether this entry describes a normal route or a mount.
	Kind RouteKind

	// AutoHead reports whether this is an auto-generated HEAD twin of a GET route.
	AutoHead bool

	// RegisteredAt is the user call site ("file:line") that registered this
	// route or mount, or "" when the framework could not capture it. Intended
	// for startup route dumps and /debug/routes diagnostics.
	RegisteredAt string
}

RouteInfo holds introspection data about a registered route or mount.

A normal route (Kind == RouteKindRoute) carries a single Method and a nil Methods. A mount (Kind == RouteKindMount) leaves Method empty and lists its sorted forwarded method set in Methods, because one mount answers every forwarded method at its prefix.

Meta is the fully resolved route metadata (route ← group ← app) as a shallow copy: callers may read it and add or delete keys without affecting framework state, but the values are read-only by convention — mutating a slice, map, or pointer stored in Meta mutates the live route metadata. Meta is nil when neither the route nor any ancestor group defines metadata.

type RouteKind

type RouteKind string

RouteKind distinguishes a normal registered route from a mounted handler prefix in introspection output.

const (
	// RouteKindRoute is a normal route registered via GET/POST/Static and friends.
	RouteKindRoute RouteKind = "route"

	// RouteKindMount is a handler subtree attached via [App.Mount]. Only its
	// prefix and forwarded method set are exposed in introspection, never its
	// internal routes.
	RouteKindMount RouteKind = "mount"
)

type Routes

type Routes interface {
	// Routes returns a slice of RouteInfo for all registered routes.
	Routes() []RouteInfo
}

Routes describes the interface for route introspection.

type Shutdowner

type Shutdowner interface {
	Shutdown(ctx context.Context) error
}

Shutdowner is implemented by services that need cleanup on shutdown. The context carries a deadline from the application's graceful shutdown timeout; implementations should respect ctx.Done() for timely cleanup.

type StaticCacheContext

type StaticCacheContext struct {
	// RequestPath is the path received from the client before internal rewrites.
	RequestPath string

	// FilePath is the resolved slash-separated path inside the fs.FS.
	// Directory listings use the resolved directory path.
	FilePath string

	// FileName is the base name of the served file, or the generated listing name.
	FileName string

	// IsHTML reports whether the response is an HTML entry point or listing.
	IsHTML bool
}

StaticCacheContext describes a successfully resolved static response for cache policy decisions.

type StaticConfig

type StaticConfig struct {
	// Index is the file served for directory requests.
	// Default: "index.html".
	Index string

	// Browse enables directory listing when a directory has no index file.
	// SPA takes precedence: if SPA is also true and the request qualifies
	// as an SPA candidate, the sibling-or-root fallback runs instead of
	// the directory listing.
	// Default: false (returns 404 for directories without index).
	Browse bool

	// SPA enables single-page application mode for routes that target a
	// page rather than an asset (GET or HEAD with no dot in the last path
	// segment). For these requests, the resolver prefers, in order:
	//
	//   File branch (path does not exist):
	//     1. <path>.html sibling (e.g. /admin/users → admin/users.html)
	//     2. root index
	//
	//   Directory branch (path is an existing directory):
	//     1. <dir>/<Index> (the directory's own index file)
	//     2. <dir>.html sibling (e.g. /reports/ → reports.html)
	//     3. root index
	//
	// Requests with a dot in the last segment (app.js, style.css) skip
	// the fallback entirely and return 404 when missing.
	// Default: false.
	SPA bool

	// Download sets Content-Disposition to "attachment" on all responses,
	// prompting the browser to download instead of display inline.
	// Default: false.
	Download bool

	// CacheControl decides the Cache-Control header for each successfully
	// served response (status below 400). It receives the resolved cache
	// context and returns the full header value; returning "" writes no
	// header. Nil disables Cache-Control entirely.
	//
	// The hook runs once per response — including 206 partial-content
	// responses — and never for error statuses. It should be pure and
	// deterministic.
	//
	// Presets cover the common policies:
	//
	//	credo.StaticConfig{CacheControl: credo.StaticCacheMaxAge(24 * time.Hour)}
	//	credo.StaticConfig{CacheControl: credo.StaticCacheImmutableAssets(365 * 24 * time.Hour)}
	//
	// Anything else is a few lines of code — e.g. SvelteKit-style builds
	// where only one directory holds content-hashed assets:
	//
	//	CacheControl: func(c credo.StaticCacheContext) string {
	//		if strings.HasPrefix(c.FilePath, "_app/immutable/") {
	//			return "public, max-age=31536000, immutable"
	//		}
	//		return "no-cache, must-revalidate"
	//	}
	CacheControl func(StaticCacheContext) string
}

StaticConfig configures static file serving for App.Static, Group.Static, App.File, and Group.File.

For App.File and Group.File, only Download and CacheControl apply. Setting Browse, SPA, or a non-empty Index panics at registration time.

Credo can only guard headers written before its Response.WriteHeader call. Reverse proxies or CDNs that add Cache-Control after Credo must be configured separately.

type StaticRoute

type StaticRoute struct {
	// contains filtered or unexported fields
}

StaticRoute represents a static file serving endpoint. It wraps the two internal GET routes created by App.Static (catch-all + exact prefix match) and proxies fluent configuration to both. HEAD requests automatically observe the same middleware and route metadata as GET, so calling StaticRoute.Middleware or StaticRoute.SetMeta keeps the two methods in sync without any extra bookkeeping.

func (*StaticRoute) BuildURI

func (sr *StaticRoute) BuildURI(filePath ...string) string

BuildURI returns the URL path for a file within this static endpoint.

sr.BuildURI("css/app.css")  → "/static/css/app.css"
sr.BuildURI("")             → "/static/"
sr.BuildURI()               → "/static/"

func (*StaticRoute) Middleware

func (sr *StaticRoute) Middleware(m ...Middleware) *StaticRoute

Middleware appends middleware to both the catch-all and exact-match GET routes. HEAD requests automatically run the same chain as GET, so auth, rate limiting, and other header-mutating middleware can never be silently bypassed via a HEAD request.

Must be called before the server starts; panics if called after compile.

func (*StaticRoute) Name

func (sr *StaticRoute) Name(name string) *StaticRoute

Name sets the route name on the primary (catch-all) route only. Route names must be unique; the exact-match route remains unnamed. Use StaticRoute.BuildURI for URL generation instead of Route.BuildURI, which requires the catch-all parameter explicitly.

Must be called before the server starts; panics if called after compile.

func (*StaticRoute) SetMeta

func (sr *StaticRoute) SetMeta(key string, val any) *StaticRoute

SetMeta sets metadata on both the catch-all and exact-match GET routes. HEAD requests automatically see the same metadata as GET, so middleware reading route meta returns consistent values for both methods and both registration patterns.

Must be called before the server starts; panics if called after compile.

type StdMiddleware

type StdMiddleware = func(http.Handler) http.Handler

StdMiddleware is a stdlib-compatible middleware signature. Use WrapStdMiddleware to convert stdlib middleware into Middleware.

type SuccessRenderer

type SuccessRenderer func(c *Context, status int, data any) error

SuccessRenderer formats a successful response, given the status code and the payload a handler wants to send. It is the success-side mirror of ErrorRenderer: opt-in, never installed by default, and consulted only through Context.Render — the raw Response helpers (Response.JSON, Response.XML, Response.Text, Response.Blob, and the streaming writers) stay un-intercepted so webhooks, health probes, and third-party response shapes always bypass any house envelope.

A non-nil error returned by the renderer flows into the normal error pipeline (classification, logging, ErrorRenderer), exactly as if the handler had returned it. The renderer owns committing the response; if it writes nothing, the framework treats the call as complete.

Register a custom renderer with App.SetSuccessRenderer. The single status+data seam is also the integration point a future typed-endpoint layer would route its typed result through, so one envelope policy covers both.

type WalkFunc

type WalkFunc func(method, pattern string) error

WalkFunc is the callback for Walk. It receives the method and pattern of each registered route. Return a non-nil error to stop walking.

type WalkRoutesFunc

type WalkRoutesFunc func(ri RouteInfo) error

WalkRoutesFunc is the callback for WalkRoutes. It receives the full RouteInfo including the host pattern. Return a non-nil error to stop walking.

Directories

Path Synopsis
Package auth provides authentication infrastructure for Credo applications.
Package auth provides authentication infrastructure for Credo applications.
Package config provides struct-centric configuration loading with deterministic merge order.
Package config provides struct-centric configuration loading with deterministic merge order.
Package fault defines transport-neutral semantic error kinds.
Package fault defines transport-neutral semantic error kinds.
Package httpclient provides outbound HTTP for Credo applications: timeout, retry with backoff, structured logging, and W3C trace context propagation — composed as an http.RoundTripper chain on a plain *http.Client.
Package httpclient provides outbound HTTP for Credo applications: timeout, retry with backoff, structured logging, and W3C trace context propagation — composed as an http.RoundTripper chain on a plain *http.Client.
internal
di
Adapted from github.com/samber/do (MIT License).
Adapted from github.com/samber/do (MIT License).
faultstatus
Package faultstatus contains Credo's default HTTP transport policy for transport-neutral semantic faults.
Package faultstatus contains Credo's default HTTP transport policy for transport-neutral semantic faults.
health
Package health defines stable, bounded health probes and the module-internal seam through which integration packages contribute store checks to the root health engine.
Package health defines stable, bounded health probes and the module-internal seam through which integration packages contribute store checks to the root health engine.
httpheader
Package httpheader provides comma-separated header token helpers shared by the root package's built-in middleware tier and the middleware package.
Package httpheader provides comma-separated header token helpers shared by the root package's built-in middleware tier and the middleware package.
httpwriter
Package httpwriter resolves optional net/http response-writer capabilities through bounded Unwrap chains.
Package httpwriter resolves optional net/http response-writer capabilities through bounded Unwrap chains.
i18n
Package i18n is the internal message lookup engine for the Credo framework.
Package i18n is the internal message lookup engine for the Credo framework.
observe
Package observe provides shared helpers for request observation code such as access logging and panic recovery.
Package observe provides shared helpers for request observation code such as access logging and panic recovery.
radix
Package radix implements a compressed radix tree (Patricia trie) for HTTP request routing.
Package radix implements a compressed radix tree (Patricia trie) for HTTP request routing.
requestid
Package requestid provides shared request ID helpers used by Credo's built-in and configurable middleware implementations.
Package requestid provides shared request ID helpers used by Credo's built-in and configurable middleware implementations.
resourceid
Package resourceid canonicalizes stable resource identity tokens shared by lifecycle-aware framework integrations.
Package resourceid canonicalizes stable resource identity tokens shared by lifecycle-aware framework integrations.
Package middleware provides built-in HTTP middleware for the Credo framework.
Package middleware provides built-in HTTP middleware for the Credo framework.
Package pagination provides generic, ORM-agnostic types and utilities for paginated API responses.
Package pagination provides generic, ORM-agnostic types and utilities for paginated API responses.
scripts
releasegate command
Command releasegate verifies Credo's lockstep multi-module release contract.
Command releasegate verifies Credo's lockstep multi-module release contract.
Package store provides universal data access contracts for the Credo framework.
Package store provides universal data access contracts for the Credo framework.
sqldb module
Package testutil provides helpers for testing Credo applications: building a hermetic test App, overriding dependencies with fakes, injecting config, and asserting on structured log output.
Package testutil provides helpers for testing Credo applications: building a hermetic test App, overriding dependencies with fakes, injecting config, and asserting on structured log output.
Package validation provides a programmatic, type-safe validation engine using Go generics.
Package validation provides a programmatic, type-safe validation engine using Go generics.
Package websocket provides Credo's server-side WebSocket adapter over the exact-pinned github.com/coder/websocket protocol engine.
Package websocket provides Credo's server-side WebSocket adapter over the exact-pinned github.com/coder/websocket protocol engine.
Package worker provides background task management for Credo applications.
Package worker provides background task management for Credo applications.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL