Imported from agentic-lab-club/smart-travel-halyk-hackathon (
backend/AGENTS.md). Install upstream withnpx skills add agentic-lab-club/smart-travel-halyk-hackathon --skill backend. Copyright stays with the author.
Golang Backend Agent Guide
Use this as the source of truth for agentic coding in backend/. It incorporates the intent of ARCHITECTURE.md.
Quick Orientation
- Go: 1.25.x (
go.mod), Fiber v3. - Data: PostgreSQL via
sqlx. - Composition root:
cmd/server/main.gowires modules, middleware, docs, metrics, and cross-module dependencies. - Modules: feature packages under
internal/with "modular monolith" boundaries. - Shared infra:
pkg/(config, db wrapper, http middlewares/responder, logger, metrics, integrations). - Migrations:
db/migrations(goose) run automatically at startup (pkg/database/postgres.go). - Swagger: generated into
docs/(pinnedswagin Dockerfile) and served at/docs/*.
Architecture Rules (What We Enforce)
cmd/server/main.gois the only place that should "know about everything" and connect modules together.internal/<module>packages should not import otherinternal/<other-module>packages.- Cross-module collaboration is done by passing dependencies at init time (service/hub handles returned from
Init(...)), not by importing modules. - Keep the layering intent: handlers are thin, services own business rules, repositories own data access. Splitting across multiple files is OK (and already used).
Pragmatic note: the repo contains both "preferred" patterns and older/one-off styles. When editing an existing module, preserve local conventions unless you are explicitly refactoring the module to the preferred patterns.
HTTP Contracts (Fiber Locals, Middleware, Responses)
Middlewares in pkg/http/middlewares establish a locals contract that handlers should rely on instead of re-parsing:
- Correlation ID: request logger sets
correlation_idand response headerX-Request-ID(pkg/logger/middleware.go). - Auth user ID:
md.AuthRole(...)setsauth_id(uuid.UUID) on success. - Params:
md.ValidateParam[T]("id")stores parsed value inLocals("id"). - Request body:
md.BindAndValidate[T]()stores parsed struct inLocals("body"). - Timeout context:
md.Timeout(...)storescontext.ContextinLocals("ctx"). - Logger: request logger stores a
*zerolog.LoggerinLocals("log"). - Config (select routes): some routes inject config using
md.AddLocals("config", cfg); handlers may then readconf := c.Locals("config").(*config.Config)(example:internal/payment/*). If you add/extend endpoints in those areas, keep the contract consistent and includemd.AddLocals("config", cfg)inmodule.go.
Responder helpers live in pkg/http/responder (import alias respond). Prefer:
respond.OK,respond.Created,respond.EmptyOK,respond.NoContentfor success.respond.ErrorStatus/respond.WithStatusfor errors.
Important: respond.Respond only emits {"error": ...} when err != nil. Passing nil error with a string "error message" returns a JSON string, not an {"error": ...} object. New code should avoid that pattern for error responses.
API + Middleware Baseline
cmd/server/main.go applies:
recoverwith stack traces- request logging + correlation IDs
- security headers + CSP overrides for
/docs - timeout + rate limit + CORS
- optional Prometheus metrics middleware + endpoint
When adding endpoints, assume these are always on and keep handlers fast (timeouts exist and are enforced).
Routing reality:
- Most HTTP endpoints live under
/api/v1/...(modules typicallyserver.Group("/api/v1")). - Some endpoints are
/api/v2/...(notably payments/courses). - Auth OTP endpoints are at
/auth-otp/.... - Non-API endpoints:
/health,/docs/*, and metrics atcfg.Metrics.Path(default/metrics).
Data Access Standards (SQL, Squirrel, Metrics)
Hard rules:
SELECT *is not allowed. Always list columns explicitly.- Errors must be wrapped with action context:
fmt.Errorf("failed to <action>: %w", err).
Preferred DB execution (for metrics):
- Use
TrackedDBmethods when possible:TrackedGet,TrackedSelect,TrackedExec("insert|update|delete", ...). - If you use raw
db.Get/db.Select/db.Exec, DB metrics will not be recorded (because tracking happens inTrackedDBwrappers).
SQL styles in this repo (all are allowed; pick one and be consistent within the touched area):
-
Embedded
.sqlfiles (common inchat,booking,payment,class,catalogue):- SQL lives under
internal/<module>/queries/*.sql - Go embeds with
//go:embed queries/<name>.sql(usually inqueries.go) - Execution typically uses
db.Rebind(query)andTracked* - Placeholders can be
?or$n; for Postgres,db.Rebindis safe either way (no-op if there are no?).
- SQL lives under
-
Squirrel builder (very common across modules):
- Use
squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)(or module-levelpsqlbuilder) - Call
ToSql(), then execute withTracked*if possible.
- Use
-
Inline SQL strings (used for some complex/legacy repos):
- Use
$nplaceholders for Postgres. - Prefer
Tracked*for reads/writes to keep DB metrics.
- Use
Transactions:
- Use
Beginx()(often exposed asBeginTx()helper) and pass*sqlx.Txthrough repository methods when making multi-step changes.
Migrations (Goose)
- Migrations live in
db/migrations/*.sqland use goose directives (-- +goose Up,-- +goose Down, etc.). - App startup runs
goose.Upautomatically (pkg/database/postgres.go), so broken migrations break boot. - Prefer timestamped filenames (existing convention:
YYYYMMDDHHMMSS_description.sql).
Template:
-- +goose Up
-- +goose StatementBegin
-- your SQL here
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
-- rollback SQL here
-- +goose StatementEnd
For changes that require schema updates, do the migration first, then update repositories/services.
Swagger / Docs
- Swagger is generated from annotations in
cmd/server/main.goand handler comments. - Handlers should be documented with Swagger comments (
@Summary,@Description,@Tags,@Router, etc.). - Tag convention: the codebase mostly uses
// @Tags @<module>(examples:@auth,@booking,@class,@user). Some older endpoints use mixed casing (e.g.Payment) or no@. New endpoints should follow@Tags @<module>consistently. - Docker build pins and runs
swag init -g cmd/server/main.goand produces environment-specific JSON (docs/swagger-*.json). - Treat
docs/*as generated artifacts. Update annotations and regenerate rather than hand-editing JSON/YAML.
Local regeneration (matching Docker pin) can be done with:
go run github.com/swaggo/swag/cmd/swag@v1.8.1 init -g cmd/server/main.go
Config
- Config is loaded via
pkg/config.Load()using-config=<env>(defaultlocal). - It reads
./config/config.<env>.yamlvia Viper and allows env overrides (dots become underscores). - In practice,
config/config.local.yamlacts like a checked-in.envequivalent for local development (but still keep secrets out of git and use real env vars when needed). - If you add new config keys, update
config/config.example.yamland any relevant docs.
Environment checks (common pattern in services):
env := strings.ToLower(strings.TrimSpace(s.config.Environment))
isProd := env == "prod" || env == "production"
Time Rules
- Use
timekit.NowUTC()for "now" and store/compare timestamps in UTC.pkg/timekitreturnstime.Now().UTC()and the server setstime.Local = time.UTCincmd/server/main.go. - If you need a user/region-specific offset, convert explicitly using a location (do not rely on local machine timezone).
Logging Standards
- Always use the per-request logger from locals:
l := c.Locals("log").(*zerolog.Logger). - Every meaningful log line should include an
eventfield:Str("event", "<domain>_<action>_<stage>"). - Common stages:
_start,_success,_failed. Includehttp_statuson failures when returning an error. - Request logger redacts body content for sensitive endpoints by emitting
body_previewas<redacted>(seepkg/logger/middleware.go). If you add new endpoints that accept secrets/PII/payment data, updateisSensitiveEndpoint()accordingly.
Agentic Workflow (Concrete Recipes)
For most feature work:
- Start from
cmd/server/main.goto see wiring and dependencies. - Open the module's
module.goto see routes + middleware used. - Keep handlers thin: read locals, call service, return via
respond.*. - Put business rules in service; keep repositories data-only.
- Pick one SQL style for the touched area and keep it consistent.
- Prefer
TrackedDBmethods to keep DB metrics working. - If you add a new endpoint, add/update tests in the same module (
*_test.go). Prefer table-driven tests and keep them focused on handler/service behavior. - Run formatting + tests you touched:
gofmtand Docker-based tests (see Docker Test Workflow below). Avoid localgo testunless explicitly required.
New endpoint checklist:
- Add middleware in
module.go(auth/param/body/config locals) instead of duplicating logic in handlers. - Add Swagger docs in the handler (
@Summary,@Description,@Tags @<module>,@Router ...). - Add structured logs with
eventnames and includehttp_statuson failures. - Add unit tests (
*_test.go) in the same module for the new behavior.
Docker Test Workflow
Preferred Docker-based test loop (fast, uses cached Go modules/builds):
make test-docker-build # build test image (only when deps change)
make test-docker # run all tests
Targeted tests without rebuilding:
docker compose run --rm --no-deps golang-test go test ./internal/payment
Long-lived test container is not recommended; prefer docker compose run --rm --no-deps for each test run.
Module Template
Use internal/_module_template/ as the canonical scaffold for new modules. Copy it to internal/<module>/, rename *.tmpl to real files, and adjust package/type/route/tag names.
Module README (required for every new module):
- Create
internal/<module>/README.mdwhen you add a module. - Keep it short and structured: purpose/scope, owned routes, external deps (other services, queues, third-party APIs), config/env keys used, migrations added, and how to run module-specific tests. Link to relevant Swagger tags (
@<module>). - Update the README when adding significant behavior, new routes, or config flags.
Avoid:
- Importing other
internal/*modules from a module. - Expanding
cmd/server/main.gobusiness logic (it should only wire things). - Introducing new one-off response shapes without a strong reason.