Kezdő lépések

Projektstruktúra

Egy generált fogyasztó projekt felépítése és a mögötte lévő szabályok.

Így néz ki a shop mintaalkalmazás a teljes kiépítés után: két surface-szel, workerrel, eventekkel és migrációkkal:

shop/
├── gpsystem.yaml                 # generátor-manifest: projektnév, module path, kit verzió,
│                                 # db (pgx|bun), template checksum, modulok+surface-ök
├── go.mod                        # require gpsystem + tool directive-ek (gpsystem, oapi-codegen)
├── mise.toml                     # taskok: setup / spec / generate / dev / restart / build / test
├── Dockerfile                    # 4 stage: base / development (bind-mount, go run) / build / production
├── .dockerignore
├── compose.yml                   # teljes dev-stack: postgres, valkey, mailpit, rustfs, migrate,
│                                 # worker, modul-service-ek, nulla publikált port, Traefik/proxynet
├── .env.example
├── spec/typespec/                # ★ az API kontraktus (source of truth)
│   ├── main.tsp                  # minden modult importál (anchorral kezelve)
│   ├── lib/                      # @permission / @policy dekorátorok
│   ├── shared/errors.tsp         # ProblemDetail + FieldError (a httperr tükre)
│   ├── shared/paginator.tsp
│   └── modules/<modul>/          # modulonként: <surface>.tsp + models/
├── api/
│   ├── openapi/                  # emittált OpenAPI 3.0 (commitolva, review-olható diffekért)
│   └── oapi-codegen/
│       ├── templates/            # a projekt codegen template-jei (upstream chi-server
│       │                         # + a kit minimális strict-felülírásai)
│       └── <modul>-<surface>.yaml
├── cmd/
│   ├── <modul>/main.go           # modulonként egy HTTP belépési pont (~20 sor)
│   ├── worker/main.go            # háttérfeldolgozó: asynq szerver + outbox relay + scheduler
│   └── migrate/main.go           # migrációfuttató
├── internal/
│   ├── platform/config/config.go # összerakott env konfig (Server + DB + Worker + Outbox [+ JWT])
│   └── modules/<modul>/
│       ├── register.go           # Dependencies struct + surface-enként egy Register<Surface>(router, deps)
│       ├── core/                 # a modul KÖZÖS üzleti logikája: entitások/nézetek, szabályok,
│       │                         # sentinel-hibák (pl. ErrNotFound); minden surface ezt használja
│       ├── repository/           # a modul EGYETLEN perzisztencia-rétege (sor-structok +
│       │                         # interfész + implementáció); nevesített kapcsolatnál repository/<kapcsolat>/
│       ├── events/               # a modul eventjei (struct + EventName)
│       ├── listeners/            # a modul worker-listenerei
│       │   ├── register.go       # events.Listen(...) regisztrációk a workerhez
│       │   └── <név>.go          # listener-törzsek
│       ├── jobs/                 # a modul worker-jobjai
│       │   ├── register.go       # sched.Job(...) regisztrációk a workerhez
│       │   └── <név>.go          # job-törzsek
│       ├── mail/                 # a modul mail-notifierjei (payload + sablonok)
│       │   └── <név>.go          # notifier-törzs + templates/<név>.{html,txt}.tmpl
│       ├── notifications/        # a modul többcsatornás notificationjei (payload + Via)
│       │   └── <név>.go          # notification-törzs + templates/<név>.{html,txt}.tmpl
│       └── surfaces/
│           └── <surface>/        # bármely név (api | web | admin | ...); az "api" a modul gyökerére mountol
│               ├── http/handler.go   # a generált szigorú interfészt implementálja
│               ├── http/mapper/      # DTO ↔ service típus mappelés (üresen tartva)
│               ├── http/gen/         # oapi-codegen kimenet, soha ne szerkeszd
│               ├── policy/           # policy registry (@permission/@policy érvényesítéshez)
│               └── service/          # vékony, célközönség-specifikus varrat: a core-ba delegál
└── migrations/                   # időbélyeg-prefixes SQL migrációk (20200101000000_init.sql, 20200101000100_outbox.sql, ...)

A worker-réteg (cmd/worker, listeners/, jobs/, events/) az új projektekben alapból ott van; ha egy projekt new project --no-worker-rel készült, az add worker adja hozzá.

Mi élhet egy modulon belül

Egy modul mappájában négyféle, egymástól jól elkülönítendő dolog lehet:

FajtaMappákMi ez
Surface-öksurfaces/api/, surfaces/admin/, ...Párhuzamos, célközönségenkénti HTTP felületkötegek: http + service + policy
Közös magcore/, repository/Amit minden surface megoszt: az üzleti szabályok és entitások (core/), plusz a modul egyetlen perzisztencia-rétege (repository/)
Trigger-egységekjobs/, listeners/Alternatív belépési pontok: idő vagy esemény hívja, HTTP helyett; a cmd/worker-be vannak bekötve
Kontraktok és tartalomevents/, mail/, notifications/Amit a modul termel: event-payloadok, üzenettartalom és sablonok
Egy surface egy célközönségenkénti felületköteg: egy publikus api, egy admin, egy web, mind ugyanannak a modulnak egy-egy párhuzamos HTTP-felülete. A core a modul közös üzleti logikája (sima structok metódusokkal, egyszer leírt szabályok), nem DDD-féle "domain layer". A surface-enkénti service a célközönség-specifikus varrat: DDD-fogalmakkal az application service szerepét tölti be, és a közös szabályokhoz a core-ba delegál. Lásd a design pattern-eket az indoklásért.

Az ökölszabály: a csatorna/driver-kód a kitben vagy az internal/platform-ban él, a tartalom a modulban, a trigger a jobs//listeners/-ben. A mail a legtisztább példa: az SMTP-transzport (gpsystem/mail, gpsystem/mail/smtp) infrastruktúra, pont úgy mint az adatbázis-driver, a kitben él. Hogy milyen e-mailt küldünk, milyen subjecttel és sablonnal, az a contact modul üzleti tartalma, az internal/modules/contact/mail/-ban él. Lásd a mail oldal kidolgozott példáját.

Ugyanez a szétválasztás egy szinttel feljebb is érvényes a notificationökre: egy notification csatornáit (mail, database, broadcast) a kit adja (gpsystem/notify, plusz a driver-specifikus gpsystem/notify/database és gpsystem/notify/broadcast), míg hogy egy notification mit mond (payload, subject, sablonok), az modultartalom az internal/modules/<modul>/notifications/-ban.

Rétegszabályok

A függőségi irány egyirányú:

http (handler) → service → core → repository
  • A handler soha nem nyúl adatbázishoz; inputot köt/validál, a saját surface-e service-ét hívja és eredményt mappel. A handler soha nem importálja a repository csomagot: a sentinel-hibákat (pl. "nincs ilyen rekord") a repository vagy a core definiálja, és a core/service exportálja tovább, így a handler mindig csak a saját surface-e service csomagját ellenőrzi (errors.Is(err, service.ErrNotFound)).
  • A surface-enkénti service vékony, célközönség-specifikus varrat: a közös szabályokhoz a core-ba delegál, és csak a saját felületének folyamatait tartja. Repository-interfészektől függ, így sima fake-ekkel tesztelhető.
  • A core a modul közös üzleti logikája: az entitások/nézetek, az egyszer leírt szabályok és a sentinel-hibák. Eseményt a szabály helyén dobunk, a tranzakción belül, a Dependencies-ben kapott dispatcheren át. A core soha nem importál surface-csomagot.
  • A modulszintű repository/ tartalmazza az SQL-t, a sor-structokat és az interfészt+implementációt, modulonként egyszer, minden surface számára közösen. pg.DBTX-et kap, így ugyanaz a kód fut poolon vagy tranzakcióban, anélkül hogy tudna róla, vagy --db bun projektben a bun megfelelője. Egy modul repositoryja egy DB engine-t használ; nevesített kapcsolatnál repository/<kapcsolat>/ almappára bomlik (add db).
  • A policy/ a surface kérés-szintű jogosultsági szabályait tartja: a policy réteg a generált enforcer middleware-en át hívja.

Két bináris, egy életciklus

A cmd/<modul> a HTTP-oldalt futtatja, a cmd/worker a háttérfeldolgozást, mindkettő ugyanarra az app életciklusra épül: azonos graceful shutdown, azonos telemetria-bekötés. Külön skálázod őket: több HTTP-replika, több worker-replika. Az outbox és a scheduler replikabiztos.

Generált vs. saját

Csak a http/gen/ újragenerált kimenet (és jelölve is van). Minden más (handlerek, service-ek, repositoryk, a .tsp fájlok, a listener/job törzsek) normál kód, amit szerkesztesz. A generátorok kizárólag // gpsystem:* anchor-kommenteknél bővítenek meglévő fájlt; lásd a CLI áttekintést.

Az api/openapi/*.yaml szándékosan commitolva van: az API-változások review-olható diffként jelennek meg a pull requestekben, és a go build soha nem függ a Node toolchaintől.

Hova kerüljön a közös kód?

Egy generált projektben nincs pkg/ és nincs internal/pkg/: az internal/ már önmagában privátat jelent, egy második réteg alatta csak egy szemetesfiókot hívna életre. Az internal/platform/config/ az egyetlen app-szintű közös mappa, amivel egy friss projekt indul. Modulon belül a több surface által megosztott szabályok helye a generált core/; lásd a Közös kód: egy döntési létra szekciót arról, hova kerüljön a több surface vagy modul által használt kód.

Copyright © 2026