chi-skeleton

module
v0.0.0-...-80b4c31 Latest Latest
Warning

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

Go to latest
Published: Sep 15, 2026 License: MIT

README

chi-skeleton

Test Lint Go Version License

A modular monolith skeleton for Go web applications, built on chi and net/http. Prefer Fiber? See fiber-skeleton.

Features

  • Modular architecture — Kratos-style biz / data / service layers per module, boundaries enforced by an architecture test
  • Compile-time DIlibtnb/wire generates plain constructors; no runtime container, no reflection
  • Databaserio on SQLite (MySQL/PostgreSQL drop in) with versioned migrations written in Go
  • Validation — request binding (chix) with a boolean rule DSL and i18n messages (validator)
  • OpenAPI 3.1 — generated from the same validate tags, served with a Scalar UI at /docs
  • Typed errors — a closed set of error kinds maps to HTTP statuses in one place (internal/shared/apperr)
  • Sessions — database-backed (sessions), started for every request except health probes
  • Logging — structured slog to a rotating file and/or stdout; access logs (httplog) share the logger
  • Scheduled jobs — cron with panic recovery and overlap skipping
  • Event bus — in-process pub/sub; modules contribute subscribers
  • WebSocket — example echo endpoint at /ws
  • Lifecycle — graceful shutdown on SIGINT/SIGTERM, zero-downtime upgrade on SIGHUP (graceful)
  • Code generation — scaffold a CRUD module or a migration with one command
  • Tests — handler tests on mocked repos, data-layer tests on a real SQLite, and an architecture test

Getting started

Requires Go 1.27.

git clone https://github.com/libtnb/chi-skeleton my-app && cd my-app
make init   # create config/config.yml from the example
make run    # or: make dev (hot reload via air)

The API listens on :3000:

curl localhost:3000/users

Project layout

cmd/            entry points: app (HTTP server), cli (management commands), gen (generator)
config/         configuration files
docs/           hand-written docs; the OpenAPI document is generated at runtime
internal/
  app/          composition root: combines modules into the app and cli injectors
  migrations/   schema history, one file per migration
  mocks/        generated repository mocks
  platform/     infrastructure assembly: bootstrap (providers), conf, server
  shared/       contracts shared by every module: transport, apperr, event, registry, job
  user/         business module
  order/        business module
storage/        runtime files: logs, sessions, SQLite database
web/            frontend code

Architecture

Each business module follows the three-layer design of Kratos:

  • biz — domain models, repository interfaces and usecases; no transport or database code
  • data — repository implementations
  • service — transport adapters: bind and validate the request, call the usecase, shape the response

HTTP handlers, CLI commands and cron jobs all call the same usecases.

Each module declares a Wire Module that provides its constructors and contributes routes, commands, jobs, subscribers and health checks. internal/app/wire.go combines the modules into the app and cli injectors; make generate writes the constructor code to wire_gen.go.

TestModuleBoundaries (internal/app/arch_test.go) fails the build when:

  • a module imports another module except through its biz package
  • a module imports app, platform or migrations
  • shared or platform imports anything above its layer

Everything under internal/ that is not app, migrations, mocks, platform or shared is a business module.

To use another module, declare an interface in your own biz package and adapt it over the other module's usecase in data (see order/biz.Users). Swapping that adapter for an RPC client turns the module into a separate service without touching its business logic.

Configuration

config/config.yml is loaded first (override the path with APP_CONFIG), then any APP_* environment variable wins over the file; a double underscore separates nesting levels:

APP_HTTP__ADDRESS=:8080 APP_LOG__OUTPUT=stdout ./app

All keys are listed in config/config.example.yml. Configuration is parsed into a struct and validated at startup.

Database

Query shapes are declared once at package level, validated with .Must() and reused concurrently; runtime values go to the terminal operations:

var userByIDQuery = rio.From[User]().Where("id = ?").Must()

_, err := userByIDQuery.DeleteAll(ctx, db, id)
Migrations

The schema lives in internal/migrations as Go code (migrate), one file per migration, applied in file-name order:

make gen-migration name=add_email_to_users_table   # scaffold a migration
go run ./cmd/cli migrate                           # apply pending migrations
go run ./cmd/cli migrate status                    # list applied and pending
go run ./cmd/cli migrate rollback --step 1         # undo the most recent

create_*_table names scaffold a create-table body; add_*_to_<table>_table names scaffold an alter body for that table.

Code generation

make gen name=article

scaffolds a full module: biz entity + repository interface, data repository, service handlers, request structs, documented routes, a create-table migration and the Wire module. Then:

  1. add article.Module to ApplicationModule.Include in internal/app/wire.go
  2. run make generate

Scheduled jobs

A job is a job.Fn provider in the module that owns it, contributed with Contribute[registry.Jobs](NewJob) (see bootstrap.Heartbeat). Specs support an optional seconds field, @every 30s and per-entry timezones. Jobs receive a context cancelled on shutdown; panics are recovered and overlapping runs are skipped.

Error handling

Usecases build client-facing errors with internal/shared/apperr:

apperr.Conflict("user.name_taken", "name already taken").In("user").Wrap(ErrNameTaken)

The kind (invalid, not_found, conflict, ...) maps to an HTTP status in transport.ErrorFrom; the code and message travel to the client, everything else — stack trace, domain, attributes — goes to the log. Errors without a kind return a bare 500 with the details kept in the log.

Observability

  • /healthz (liveness) and /readyz (readiness, pings the database); the Dockerfile ships a matching HEALTHCHECK
  • access and application logs share one logger; every response carries an X-Request-Id
  • http.debug_address serves pprof and expvar on a separate private port
  • framework errors (404, 405, panics) return the same JSON envelope as the API

Serving a frontend

Put the built frontend under web/ and serve it from NewRouter (internal/platform/server/server.go):

r.Handle("/*", http.FileServer(http.Dir("./web/dist")))

Deployment

make build produces static app and cli binaries in bin/ with the version injected. A Dockerfile is included; mount config/ and storage/.

Signal Behavior
SIGINT / SIGTERM drain requests and jobs (30s cap), then close resources in reverse order
SIGHUP zero-downtime binary upgrade

Development

make help       # list all targets
make generate   # regenerate Wire constructors and mocks
make lint       # golangci-lint
make test       # go test -race with coverage

Credits

Inspired by Standard Go Project Layout, Kratos, Goravel, GinSkeleton and gin-layout.

License

MIT

Directories

Path Synopsis
cmd
app command
cli command
gen command
Command gen scaffolds a CRUD module (biz entity + repo interface, data repo implementation, service handlers, request structs, create-table migration and Wire module) or a standalone schema migration.
Command gen scaffolds a CRUD module (biz entity + repo interface, data repo implementation, service handlers, request structs, create-table migration and Wire module) or a standalone schema migration.
internal
app
migrations
Package migrations is the schema history: one file per migration, named after the migration it registers, applied in lexical order.
Package migrations is the schema history: one file per migration, named after the migration it registers, applied in lexical order.
order/biz
Package biz holds the order module's business logic.
Package biz holds the order module's business logic.
order/service
Package service adapts HTTP to the usecase.
Package service adapts HTTP to the usecase.
platform/bootstrap
Package bootstrap provides the boot-time infrastructure providers.
Package bootstrap provides the boot-time infrastructure providers.
platform/server
Package server assembles the HTTP layer from the modules' route contributions.
Package server assembles the HTTP layer from the modules' route contributions.
shared/apperr
Package apperr standardizes client-facing errors: transports map the closed set of kinds to HTTP statuses.
Package apperr standardizes client-facing errors: transports map the closed set of kinds to HTTP statuses.
shared/event
Package event is the in-process event bus contract.
Package event is the in-process event bus contract.
shared/job
Package job is the scheduler contribution contract shared by every module.
Package job is the scheduler contribution contract shared by every module.
shared/registry
Package registry defines the typed collections assembled by wire.
Package registry defines the typed collections assembled by wire.
shared/transport
Package transport holds the HTTP helpers shared by every service layer.
Package transport holds the HTTP helpers shared by every service layer.
user/biz
Package biz holds the user module's business logic.
Package biz holds the user module's business logic.
user/service
Package service adapts HTTP and CLI to the usecase.
Package service adapts HTTP and CLI to the usecase.

Jump to

Keyboard shortcuts

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