module

package
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 7, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package module is the unit of organisation: a feature slice that declares what it provides, routes, owns and needs checked.

The shape is three levels and no more:

App     a bounded domain          catalog, billing, identity
Module  one resource in it        products, categories, reviews
Layer   controller / service / repository / dto / entity

A module owns exactly one resource. It declares what it provides, what it routes, which tables it owns and what it needs checked for health — and nothing outside it may reach into its service or its entities. The enforcement is social, plus `icpt lint`, plus the fact that a module's dependencies are explicit in its constructor rather than implicit in a shared package.

Why bother, in a language with packages? Because a package boundary says nothing about ordering, lifecycle or ownership. This says: these providers are constructed before those routes exist, this module's shutdown runs before its dependencies', and these two tables belong to this code and no other.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MountApp

func MountApp(r fiber.Router, a App, opt MountOptions) error

MountApp mounts one domain at /<app name> and each of its modules at /<app name>/<module name>.

The path is derived from the names rather than configured per route, which is the trade this framework makes everywhere: give up the freedom to put a route anywhere, get a URL you can predict from the directory you are standing in.

func MountModules added in v1.1.0

func MountModules(r fiber.Router, appName, prefix string, modules []Module, opt MountOptions) error

MountModules mounts a domain's modules directly on r without adding the domain name to the URL. This is useful for a host application whose public API is already rooted at a version prefix while modules retain native ownership, guards, health, lifecycle, and route metadata.

func OwnedEntities

func OwnedEntities(apps ...App) map[string][]any

OwnedEntities maps "app.module" to the tables it declares.

Two modules claiming one table is a design error that shows up as a mysterious write from the "wrong" service months later; this is what makes it checkable.

func Register

func Register(c *di.Container, descriptors ...Descriptor) error

Register runs every descriptor's Providers, in order, before anything is constructed.

Registration is separate from construction and construction is lazy, so two domains may depend on each other's services regardless of the order they are listed in — and a genuine cycle between them is reported as a cycle rather than resolved by luck.

func Seed

func Seed(ctx context.Context, apps ...App) error

Seed runs every module's Seeder, in declaration order, stopping at the first failure. Call it after migrations and before serving.

Types

type App

type App interface {
	Name() string
	Modules() []Module
}

App is a bounded domain: a group of modules under one path segment.

func Build

func Build(s *di.Scope, descriptors ...Descriptor) ([]App, error)

Build constructs every domain. Call it only after Register.

type Base

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

Base implements the boilerplate of Module. Embed it:

type Module struct {
    module.Base
    ctrl *controller.Product
}

func New(s *di.Scope) *Module {
    ctrl := controller.New(di.MustResolve[*service.Product](s))
    return &Module{Base: module.NewBase("products",
        module.WithControllers(ctrl),
        module.WithEntities(&entity.Product{}),
    )}
}

func NewBase

func NewBase(name string, opts ...Option) Base

NewBase builds the embedded base of a module.

func (Base) Controllers

func (b Base) Controllers() []router.Controller

Controllers implements Module.

func (Base) Entities

func (b Base) Entities() []any

Entities implements EntityOwner.

func (Base) Guards

func (b Base) Guards() []guard.Guard

Guards implements Guarded.

func (Base) HealthChecks

func (b Base) HealthChecks() []health.Checker

HealthChecks implements HealthContributor.

func (Base) Interceptors

func (b Base) Interceptors() []interceptor.Interceptor

Interceptors implements Intercepted.

func (Base) Name

func (b Base) Name() string

Name implements Module.

type BaseApp

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

BaseApp implements the boilerplate of App.

func NewApp

func NewApp(name string, modules ...Module) BaseApp

NewApp builds a domain from its modules. Order matters only for lifecycle and shutdown; routing is by path.

func (BaseApp) Modules

func (a BaseApp) Modules() []Module

Modules implements App.

func (BaseApp) Name

func (a BaseApp) Name() string

Name implements App.

type Descriptor

type Descriptor struct {
	// Providers registers everything the domain's modules resolve. It runs
	// for every descriptor before any Build runs.
	Providers func(c *di.Container) error
	// Build constructs the domain. By the time it runs, every provider in
	// the application is registered.
	Build func(s *di.Scope) App
}

Descriptor is how a domain is handed to the application: as two functions rather than as a constructed value.

The split is not ceremony, it is a correctness requirement. Building a module constructs its controller, which resolves its service out of the container — so every provider in the WHOLE application must be registered before ANY module is built. One value that both registers and constructs cannot express that ordering: whichever domain is listed first would be built before the second had registered anything, and would fail to resolve a dependency that exists.

func App() module.Descriptor {
    return module.Descriptor{
        Providers: Providers,
        Build: func(s *di.Scope) module.App {
            return &App{BaseApp: module.NewApp("catalog", products.New(s))}
        },
    }
}

type EntityOwner

type EntityOwner interface {
	Entities() []any
}

EntityOwner declares the tables this module owns.

It is metadata, not migration: schema comes from versioned SQL, because AutoMigrate against a production database is a way to lose a column. What the list buys you is an answerable question — "who owns this table?" — and a lint rule that catches two modules claiming the same one.

type Guarded

type Guarded interface {
	Guards() []guard.Guard
}

Guarded declares guards applied to every route in the module.

type HealthContributor

type HealthContributor interface {
	HealthChecks() []health.Checker
}

HealthContributor declares the dependencies this module needs in order to work. They are aggregated into the service-wide readiness report.

type Intercepted

type Intercepted interface {
	Interceptors() []interceptor.Interceptor
}

Intercepted declares interceptors wrapping every route in the module.

type Module

type Module interface {
	// Name is the path segment and the registry key. Lower-case, singular or
	// plural but consistent across the codebase: "products", not "ProductsModule".
	Name() string
	// Controllers declare the routes. A module with no HTTP surface — a pure
	// background worker — returns nil and implements lifecycle.Ready instead.
	Controllers() []router.Controller
}

Module is one resource: its routes, and whatever of the optional interfaces below it needs.

type MountOptions

type MountOptions struct {
	// Prefix is the path r already sits at, for the recorded route table.
	Prefix string
	// Routes records every mounted route. Strongly recommended: it is what
	// `icpt routes` and the unguarded-route test read.
	Routes *router.Registry
	// Health collects each module's readiness checks.
	Health *health.Registry
	// Lifecycle collects each module's init/ready/shutdown hooks.
	Lifecycle *lifecycle.Registry
	// Guards apply to every route of every module mounted in this call.
	Guards []guard.Guard
	// Interceptors wrap every route of every module mounted in this call.
	Interceptors []interceptor.Interceptor
}

MountOptions configures mounting.

type Option

type Option func(*Base)

Option configures a Base.

func WithControllers

func WithControllers(c ...router.Controller) Option

WithControllers attaches controllers.

func WithEntities

func WithEntities(e ...any) Option

WithEntities declares owned tables.

func WithGuards

func WithGuards(g ...guard.Guard) Option

WithGuards applies guards to every route in the module.

func WithHealthChecks

func WithHealthChecks(c ...health.Checker) Option

WithHealthChecks contributes readiness checks.

func WithInterceptors

func WithInterceptors(i ...interceptor.Interceptor) Option

WithInterceptors wraps every route in the module.

type Seeder

type Seeder interface {
	Seed(ctx context.Context) error
}

Seeder inserts reference data. It runs after migrations, so the tables exist, and it MUST be idempotent: it runs on every boot.

Jump to

Keyboard shortcuts

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