Koncepciók

Codegen pipeline

TypeSpec → OpenAPI 3.0 → szigorú, típusos szerverkód chi/net-http-n.

A gpsystem projektek API-kontraktusa spec-first: a source of truth a TypeSpec, abból fordul OpenAPI, abból pedig szigorú ("strict") szerver-interfész. Így egy spec-eltérés nem futásidejű meglepetés, hanem fordítási hiba:

spec/typespec/**.tsp ──tsp compile──▶ api/openapi/<Namespace>.yaml
        │                                     │
        │ source of truth                     │ oapi-codegen v2.8.x
        │ (kézzel + generátorokkal)           │ + engine-specifikus template-ek
        ▼                                     ▼
  gpsystem CLI                internal/modules/<m>/surfaces/<s>/http/gen/api.gen.go

A mise run generate futtatja a teljes láncot: tsp compilego generate ./...go mod tidygo build ./.... A go:generate direktíva a surface http/handler.go-jában él, és az api/oapi-codegen/<modul>-<surface>.yaml konfigot hajtja (a surfaces/ wrapper miatt a benne hivatkozott relatív útvonalak hat szinttel lépnek feljebb: ../../../../../../). Ez a fájl csak a Handler structot, a New-t és az interfész-assertiont tartalmazza; maguk a handler-metódusok kategóriánként egy fájlban élnek (<modul>_handler.go, vagy <modul>_<csoport>_handler.go egy olyan kategóriának, mint notifications), így az add handler futtatása műveletek szerint csoportosít, nem szór szét egy fájlt függvényenként (lásd add handler).

TypeSpec konvenciók

  • Minden surface saját @service @server("/api/v1<base>")-zel. Az emitter a kimeneti fájlt a namespace-ről nevezi el, ezért a namespace-ek determinisztikusak: Project.Module az api surface-re, Project.Module<Surface> minden másra (a shopban: Shop.Shop és Shop.ShopAdmin).
  • A shared/errors.tsp a httperr.Problem tükre (Hibamodell); a shared/paginator.tsp a paginate.Page[T] / paginate.CursorPage[T] tükre (Lapozás). Tartsd őket szinkronban a Go-oldallal.
  • Az OpenAPI kimenet 3.0-ra van rögzítve (az oapi-codegen nem olvas 3.1-et).
Soha ne ágyazz egy @service namespace-t egy másikba (pl. Shop.Shop.Admin a Shop.Shop alá). A TypeSpec összefésüli a route-jaikat, és duplicate-operation hibával áll le. A generátorok mindig testvér namespace-eket emittálnak.

Egy művelet útja: a shop terméklistája

1. A kontraktus. A shop api surface-ének .tsp-jében: az add handler a // gpsystem:operations anchorhoz illeszt, de kézzel is ugyanide írsz:

spec/typespec/modules/shop/api.tsp
@tag("shop")
interface ShopApi {
  // gpsystem:operations
  @get
  @route("/products")
  @operationId("ListProducts")
  @summary("List products")
  listProducts(...CursorQuery): CursorPage<Product>;
}

A CursorQuery / CursorPage<T> a scaffoldolt shared/paginator.tsp-ből jön: cursor-lapozott lista egyetlen sorban.

2. A generált interfész. A tsp compile után az oapi-codegen egy strict interfészt emittál a gen csomagba: a request/response típusnevekben nincs Object utótag, és a szignatúra semmi mástól nem függ, csak a context.Context-től és sima Go típusoktól:

internal/modules/shop/surfaces/api/http/gen/api.gen.go (generált, ne szerkeszd)
type StrictServerInterface interface {
    // (GET /products)
    ListProducts(ctx context.Context, request ListProductsRequest) (ListProductsResponse, error)
    // ...
}

3. A handler. Ezt már te írod (a vázat az add handler adja):

internal/modules/shop/surfaces/api/http/shop_handler.go
func (h *Handler) ListProducts(ctx context.Context, req gen.ListProductsRequest) (gen.ListProductsResponse, error) {
    page, err := h.svc.ListProducts(ctx, req.Params.Cursor, req.Params.Limit)
    if err != nil {
        return nil, service.ErrListProducts.Wrap(err, "api: list products")
    }
    return gen.ListProducts200JSONResponse(page), nil
}

4. A bekötés. A generált drótozás a register.go-ban:

internal/modules/shop/register.go
func RegisterApi(router chi.Router, deps Dependencies) {
    apiSvc := apiservice.New()
    apiHandler := apihttp.New(apiSvc)
    apiPolicies := apipolicy.New()
    router.Route("/shop", func(r chi.Router) {
        shopapigen.HandlerWithOptions(shopapigen.NewStrictHandlerWithOptions(
            apiHandler,
            []shopapigen.StrictMiddlewareFunc{
                server.StrictValidator[shopapigen.StrictHandlerFunc](nil),
                // Az utolsó a legkülső: az authorizáció a validáció előtt fut.
                policy.Enforcer[shopapigen.StrictHandlerFunc](apiPolicies, shopapigen.PermissionByOperation, shopapigen.PolicyByOperation),
            },
            shopapigen.StrictHTTPServerOptions{
                RequestErrorHandlerFunc:  httperr.WriteBadRequest,
                ResponseErrorHandlerFunc: server.WriteError,
            },
        ), shopapigen.ChiServerOptions{
            BaseRouter:       r,
            ErrorHandlerFunc: httperr.WriteBadRequest,
        })
    })
}

A kérésvalidáció a server.StrictValidator strict middleware-ként fut, a lánc legbelső elemeként; a hibarenderelés (mind a validációból/dekódolásból, mind egy visszaadott üzleti hibából) a server.WriteError-on és a httperr.WriteBadRequest-en megy át, a generált kód hibahookjaiba kötve. A server.WriteError az auth/rbac/policy sentineleket (lásd Policy-k) Problemre képezi, mielőtt mindent mást a httperr.WriteError-ra delegálna. Az üzleti kódodat egyik sem érinti.

A template-készlet: upstream routing + minimális strict override

A projektek az oapi-codegen beépített chi-server routing template-jeit használják (ezek naprakészek upstreamben), plusz egy minimális kit-override-ot (internal/templates/oapichi/): a két strict template-et (strict/strict-interface.tmpl, strict/strict-http.tmpl) egyetlen érdemi eltéréssel: a generált request/response típusnevekről lekerül az Object utótag (ListProductsRequest, nem ListProductsRequestObject), így az általad írt üzleti handler-kód mindenhol mentes marad ettől az utótagtól.

Hogyan marad ez biztonságos

Az override a kit repóban van pinelve és tesztelve, egy kontraktus-teszttel, ami a register-snippet által használt azonosítókat, a StrictHandlerFunc alakját és az Object-mentes neveket pineli, és lefordítja a generált csomagot. Az oapi-codegen verzióemelése először ezt a tesztet töri, sosem egy fogyasztó projektet.

Deklaratív authorizáció: @permission és @policy

A projekt-scaffold egy kis decorator-libet szállít (spec/typespec/lib/policy.tsp + policy.js), így minden surface .tsp-ben elérhető a @permission("...") és a @policy("..."). A lánc végig gépi:

@permission("orders.place") / @policy("orders.owner") a műveleten
  → x-permission / x-policy extension az emittált OpenAPI-ban (setExtension)
  → a strict template PermissionByOperation / PolicyByOperation mapeket generál a gen csomagba
  → a register.go-ba kötött policy.Enforcer middleware
    RBAC-permissiont ellenőriz, és a surface policy/ registry-jéből futtatja a policy-t

A generált mapek így néznek ki:

gen/api.gen.go (generált)
var PermissionByOperation = map[string]string{
    "PlaceOrder": "orders.place",
}

var PolicyByOperation = map[string]string{
    "GetOrder": "orders.owner",
}

Az enforcer a strict-middleware lánc legkülső eleme: már a teljesen bekötött, típusos requestet látja, a belépett usert a contextből olvassa, és 401/403-mal rövidre zár, mielőtt az üzleti handler futna. Fail-closed: a @policy-val megjelölt, de a registry-ben nem regisztrált policy nem átenged, hanem hibázik. A szemantikát és a policy-írást a Policy-k oldal írja le.

A gpsystem.yaml manifest

A projekt gyökerében élő gpsystem.yaml jelöli ki a projekt-gyökeret a CLI-nek, és rögzíti, mit generált eddig:

gpsystem.yaml
version: 1
name: shop
module: github.com/acme/shop
kit: github.com/gp-system/gpsystem
db: pgx              # vagy bun
templatesSHA: 3f2a…  # a bemásolt oapi template-készlet checksumja
modules:
  shop:
    surfaces: [api, admin]
    events: [orderPlaced]
  • A db dönti el, melyik template-fát renderelik a generátorok (add surface, add handler, ...); a választás projekt-létrehozáskor történik.
  • A templatesSHA alapján ismeri fel az upgrade templates, hogy módosítottad-e lokálisan a bemásolt template-eket: módosítottat csak --force-szal ír felül.
  • A modules bejegyzésekből tudja az add surface, hogy mi létezik már, és az add listener, hogy létező eventre iratkozol-e fel.

A // gpsystem:* anchorok

A generátorok meglévő fájlt soha nem írnak felül, kizárólag megjelölt anchor-kommenteknél illesztenek be. A fontosabbak:

AnchorFájlKi illeszt ide
gpsystem:modulesspec/typespec/main.tspnew module / add surface
gpsystem:operations<surface>.tspadd handler
gpsystem:surfaces, gpsystem:importsregister.gonew module / add surface
gpsystem:registrationscmd/<modul>/main.goadd surface
gpsystem:dependencies, gpsystem:dependencies-initregister.go / entrypointokadd event (dispatcher-bekötés)
gpsystem:listeners, gpsystem:jobslisteners/register.go / jobs/register.goadd listener / add job
gpsystem:configinternal/platform/config/config.gojövőbeli konfig-bővítések

Az illesztések idempotensek (kétszer futtatva nem duplikálnak), és a fájl goimports-szal formázódik utánuk. Ha egy anchort kitörölsz, az érintett generátor hangosan hibázik, nem próbál tippelni: a hibaüzenet előtt kiírja a beillesztendő snippetet és a visszaállítandó anchor-sort, hogy kézzel be tudd illeszteni a megfelelő helyre. Az anchorokat hagyd a helyükön, ahogy a mintaalkalmazás is teszi.

Használt patternek

Kapcsolódó oldalak

  • add handler: művelet scaffoldolása a .tsp-be és a handlerbe.
  • Validáció: hogyan validál a generált binding.
  • Policy-k: az enforcer és a policy-registry.
Copyright © 2026