kra-new/CLAUDE.md

6.7 KiB

Repository Guidelines

This is a Kratos service template. This file owns the layering contract agents must follow when changing the template.

Project structure

cmd/                      Entrypoint, Wire injector, main.go.
configs/                  Runtime config (config.yaml). No secrets.
internal/config/          Viper config models, loading, snapshots, and reloads.
internal/server/          Gin/Kratos handlers, middleware, routers, static files.
internal/service/dto/     Hand-written request and response DTOs.
internal/service/         DTO/DO transport adapters.
internal/biz/              Domain models, usecases, repo interfaces, errors.
internal/data/             Repo implementations, database clients, migrations.
internal/initialize/       First-install and configuration orchestration.
internal/integration/      External I/O adapters (cache, email, payment, storage).
internal/modules/          Built-in module schema, seed, menu, API, and task contributions.
internal/routecatalog/     HTTP route metadata and runtime policies.
internal/worker/           Timed-task runtime and scheduler.
internal/utils/            Stateless internal helpers.
pkg/                       Reusable infrastructure packages.
web/                       Vue administration frontend.

Layering & dependency rules

Three model shapes flow through three layers. biz owns the DO, data owns the PO; service is a pass-through that converts at its boundary.

   client ──► DTO ──► service ──► DO ──► biz ──► DO ──► data ──► PO ──► storage
                                  ▲                ▲
                                  │ declares       │ implements
                                  └─── repo IF ────┘

   DTO  Data Transfer Object — hand-written HTTP request / response.
   DO   Domain Object        — pure biz model, no proto, no storage tags.
   PO   Persistent Object    — storage shape, owned by `data`.
Layer Owns Speaks at boundary Never speaks
service DTO ↔ DO PO, storage client
biz DO DO DTO, PO, storage client
data PO DO ↔ PO DTO
  • service imports internal/service/dto (DTO) and biz (DO). Never data.
  • biz never imports service or data. The repo interface declared here is the inversion seam.
  • data imports biz to implement the repo interface. Never service, never DTOs.
  • integration implements external I/O boundaries and may import biz and provider SDKs; it is not a utility layer.
  • cmd is the only place that wires all layers via Wire.

A change crossing these arrows the wrong way is a layering bug; fix the design rather than add the import.

Layer responsibilities

service (DTO ↔ DO)

  • convert<Resource> parses an incoming proto into a DO. The reverse direction is built inline at the return site; the reply type is whatever the proto declares — usually the resource itself (return &v1.<Resource>{...}, nil), sometimes a list wrapper (*v1.<Resources>Set), or &emptypb.Empty{} for deletes. Inlining keeps each handler self-contained.
  • Embed Unimplemented<Resource>ServiceServer.
  • Parse AIP list requests via filtering / ordering / pagination; apply fieldmask.Update for partial updates.
  • Validate request inputs at the service boundary before delegating to the usecase.
  • Return biz errors. No business rules, no storage access, no PO.

biz (DO only)

  • Owns the DO (type <Resource> struct — no proto, no storage tags), the usecase, and the repo interface (type <Resource>Repo interface).
  • Owns typed errors built with errors.NotFound / errors.BadRequest plus the API error reason enum.
  • Owns ListOption helpers — ListFilter, ListOrderBy, ListOffset, ListLimit — so callers compose queries without leaking storage primitives.

data (DO ↔ PO)

  • Repo shape: implement biz.<Resource>Repo. The constructor returns the interface, never the concrete type: func New<Resource>Repo(d *Data) biz.<Resource>Repo.
  • PO and conversion: define a PO when the storage shape diverges from the DO. PO types stay inside data. Use free functions new<Resource> (DO → PO, write) and toBiz (PO → DO, read). Driver-specific builder types never leave data.
  • Shared clients: *Data (declared in internal/data/data.go) holds long-lived storage clients. Repos receive *Data and never construct their own clients.
  • Querying: translate ListOptions.Filter and ListOptions.OrderBy into the storage driver's query language inside the repo.
  • Errors: map driver errors to biz typed errors so callers above never branch on the driver.

server

  • Construct HTTP/gRPC servers, apply middleware, register services. No translation, no business logic.

Add-a-resource checklist

  1. DTO: define Create<Resource> / Get<Resource> / List<Resources> / Update<Resource> / Delete<Resource> in api/<domain>/<version>/, then make api.
  2. DO + repo interface: declare both in biz; build the usecase on top of the interface.
  3. Repo impl: implement in data returning biz.<Resource>Repo; add a PO and the matching conversion helpers when storage shape diverges from DO.
  4. Wiring: register the repo constructor in data.ProviderSet, the usecase in biz.ProviderSet, the service in service.ProviderSet; register HTTP/gRPC services in internal/server.
  5. Regenerate: make all to refresh Wire and go.mod.

Testing seam

Tests live beside the code they cover (*_test.go). Test layers in isolation: service tests fake the usecase, biz tests fake the repo, data tests exercise repo implementations at the storage boundary.

Generation & generated files

Regenerate via make api or make all; never hand-edit *.pb.go, *_grpc.pb.go, *_http.pb.go, or wire_gen.go.

Naming & error reasons

  • Resource: <Resource> (e.g., Todo); collection RPC: List<Resources>.
  • Types: repo <Resource>Repo, usecase <Resource>Usecase, service <Resource>Service. PO types live inside internal/data/; pick a name that fits the storage driver and convert with new<Resource>(do) / toBiz(po) free functions.
  • Error reasons: declared in api/<domain>/<version>/error_reason.proto, surfaced as Err<Resource><Cause> in biz.

Commits & security

  • Conventional Commits: feat:, fix:, refactor:, chore(deps):, docs:, test:. Regenerated files belong in the same commit as their source.
  • Never commit real credentials in configs/config.yaml.