Open-Source Wikis

/

Caddy

/

How to contribute

/

Patterns and conventions

caddyserver/caddy

Patterns and conventions

The Caddy codebase has a small number of patterns that are followed almost everywhere. If you internalize them, most of the code reads the same way regardless of which subsystem you are in.

Module registration

Every pluggable thing in Caddy is a module, and modules look the same:

func init() {
    caddy.RegisterModule(MyModule{})
}

func (MyModule) CaddyModule() caddy.ModuleInfo {
    return caddy.ModuleInfo{
        ID:  "namespace.category.name",
        New: func() caddy.Module { return new(MyModule) },
    }
}

init() runs at package import time, so importing the package is enough to make the module available. The standard build's import list is modules/standard/imports.go (and modules/caddyhttp/standard/imports.go for HTTP-specific modules).

Module lifecycle

New() → JSON unmarshal → Provision()Validate() → use → Cleanup().

Implement only the interfaces you need. Common ones (all in caddy.go):

type Provisioner interface { Provision(Context) error }
type Validator    interface { Validate() error }
type CleanerUpper interface { Cleanup() error }

For HTTP handlers there are also interfaces from modules/caddyhttp/caddyhttp.go:

type MiddlewareHandler interface {
    ServeHTTP(http.ResponseWriter, *http.Request, Handler) error
}

Interface guards

Compile-time assertions that a module implements the interfaces it claims to:

var (
    _ caddy.Provisioner     = (*MyModule)(nil)
    _ caddy.Validator       = (*MyModule)(nil)
    _ caddyfile.Unmarshaler = (*MyModule)(nil)
)

Place these at the bottom of the file. They cost nothing at runtime and make it impossible to silently break a contract.

Error flow

From AGENTS.md and consistent across the codebase: early return, no else branches.

if err != nil {
    return err
}
// normal path

Error strings are lowercase and end without punctuation:

return fmt.Errorf("decoding caddyfile: %w", err)

Wrap errors with %w so callers can errors.Is/errors.As on them. Don't panic for normal error handling — the only panics in the codebase are for programmer mistakes (e.g. registering a module without an ID in modules.go).

Logging

Use ctx.Logger() to obtain a module-scoped zap logger inside Provision:

func (m *MyModule) Provision(ctx caddy.Context) error {
    m.logger = ctx.Logger()
    m.logger.Debug("provisioning", zap.String("field", m.Field))
    return nil
}

Context.Logger() returns a logger named after the module's ID, so log lines automatically carry that field. Don't store caddy.Context in struct fields — it's tied to the lifetime of one provisioning round.

Caddyfile support

If your module should be configurable via Caddyfile, implement UnmarshalCaddyfile(*caddyfile.Dispenser):

func (m *MyModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
    d.Next() // consume directive name
    for d.NextArg() {
        // inline arguments
    }
    for nesting := d.Nesting(); d.NextBlock(nesting); {
        switch d.Val() {
        case "subdir":
            if !d.NextArg() {
                return d.ArgErr()
            }
            m.Field = d.Val()
        default:
            return d.Errf("unrecognized subdirective: %s", d.Val())
        }
    }
    return nil
}

The Dispenser is a token-stream iterator with helpers for nesting, errors, and argument counting (caddyconfig/caddyfile/dispenser.go). HTTP handler directives also need to be ordered in caddyconfig/httpcaddyfile/directives.go.

Naming

Follow Go's standard rules (AGENTS.md):

  • Initialisms stay capitalized: URL, HTTP, ID — never Url, Http, Id.
  • Receiver names are 1–2 letters reflecting the type: c for Client, h for Handler, m for module structs in some places.
  • Doc comments on exported types/functions start with the name being documented, in full sentences.

Empty slices

var t []string (nil) is preferred over t := []string{} (non-nil zero-length) unless you actually need the non-nil version (e.g. for JSON marshaling that distinguishes the two).

Storing context

Don't store caddy.Context in struct fields after Provision returns. If you need a context.Context later, derive one from a Server or take it from the request.

Admin endpoints

Modules that want their own admin route implement caddy.AdminRouter:

func (m *MyModule) Routes() []caddy.AdminRoute {
    return []caddy.AdminRoute{{
        Pattern: "/myfeature/",
        Handler: caddy.AdminHandlerFunc(m.handleAdmin),
    }}
}

The pattern must end in / if you want subpaths. See modules/caddyhttp/reverseproxy/admin.go for an example.

Dependencies

From AGENTS.md:

  • Avoid new dependencies. Tiny ones can be inlined.
  • Do not export types defined by external packages. A Caddy public API must stay independent of the upstream library shape.
  • Run go mod tidy before committing.

Where these conventions are enforced

.golangci.yml enables linters that catch some of these (govet, staticcheck, unparam). The rest are enforced by review.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Patterns and conventions – Caddy wiki | Factory