一键导入
adapter
Outbound boundary translators. One adapter per third-party system / queue / storage backend; narrow interface, vendor-neutral callers.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Outbound boundary translators. One adapter per third-party system / queue / storage backend; narrow interface, vendor-neutral callers.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Visual-design discipline for forge frontends — brief the work before building (never invent an aesthetic without user input), declare the design system before coding, lean on the component library, use restrained color/type systems, never hand-draw complex SVGs, and verify visually before declaring done.
Write Next.js frontends — generated hooks, component library, Tailwind v4, visual verification, and Connect RPC clients.
Write Connect RPC handlers — proto-driven codegen, the thin-translation handler pattern (validate, extract auth, convert proto↔internal, call service, wrap errors via `svcerr.Wrap`), middleware, and testing. Business logic lives in `internal/handlers/<svc>/contract.go`, never in handlers.
Forge project conventions and architecture — project structure, generated vs hand-written code, the generate pipeline, proto annotations, contracts, wiring, and naming.
Use-case orchestrators that compose two or more adapters/services. Deps are interfaces only — designed for unit tests with all-mock collaborators.
Proto file reference — annotations, CRUD conventions, field rules, and common mistakes.
| name | adapter |
| description | Outbound boundary translators. One adapter per third-party system / queue / storage backend; narrow interface, vendor-neutral callers. |
| emit | both |
An adapter is the package that translates between your domain and one external system: a third-party HTTP API, a message broker, a storage gateway, an OAuth provider. It owns the wire format, the retries, the timeout policy, and the response mapping. It does not own business logic — that lives one layer up.
Add an adapter the moment you need to call an external system that isn't your own database. Symptoms:
POST https://api.stripe.com/v1/... (or the equivalent in your stack) inline in a service.If the external system is your own first-party API that you already generate clients for, you don't need an adapter — depend on the generated client. Adapters exist for boundaries you don't already manage.
The adapter exposes ONE narrow interface in the language of your domain — not the full vendor SDK. Conventions worth keeping:
Illustrative shape (Go):
type Service interface {
HealthCheck(ctx context.Context) error
CreateCharge(ctx context.Context, in CreateChargeInput) (CreateChargeResult, error)
}
type CreateChargeInput struct {
AmountMinor int64
Currency string
CustomerID string
}
type CreateChargeResult struct {
ChargeID string
}
Same shape in any language: a narrow interface in domain types, hiding the vendor's wire format.
Declare interfaces at the consumer, not the implementation. The adapter's exported Service interface is a legitimate seam — it's the mock/test target and the DI boundary (accept interfaces, return concrete structs is honoured by forge's New(Deps) Service). But keep it narrow: one method per use case the domain actually needs. If two different consumers each use a different slice of a wide adapter interface, that's a smell — let each consumer declare the 1–2 method interface it needs at its own site (see the role-interface pattern in the api skill), so widening the adapter never breaks a consumer that didn't ask for the new method.
// Adapter exposes a narrow, domain-worded Service (the mock seam).
type Service interface { CreateCharge(ctx context.Context, in CreateChargeInput) (CreateChargeResult, error) }
// A consumer that only needs one method declares its own view, next to itself:
type charger interface { CreateCharge(ctx context.Context, in CreateChargeInput) (CreateChargeResult, error) }
Adapter tests use an in-process HTTP test server (or the SDK's record-and-replay equivalent) to stand in for the vendor. The point is to exercise the adapter's translation logic — request construction, header injection, response parsing, error mapping — against a controlled downstream.
func TestCreateCharge_OK(t *testing.T) {
stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/charges" { http.NotFound(w, r); return }
if got := r.Header.Get("Authorization"); got == "" {
t.Errorf("missing Authorization header")
}
_, _ = w.Write([]byte(`{"id":"ch_test_1"}`))
}))
defer stub.Close()
svc := New(Deps{HTTPClient: stub.Client(), BaseURL: stub.URL})
res, err := svc.CreateCharge(context.Background(), CreateChargeInput{...})
if err != nil { t.Fatalf("CreateCharge: %v", err) }
if res.ChargeID != "ch_test_1" { t.Fatalf("got %q", res.ChargeID) }
}
The interactor that calls this adapter in production mocks the interface, never the downstream HTTP server.
Your adapter wraps whatever client you choose — raw HTTP, the vendor SDK, an RPC client, a queue / streaming client. The convention is the shape of the package (narrow interface in domain types), not the transport library.
Adapters are leaf nodes at construction, but occasionally a downstream consumer registers a callback / sink / subscriber onto the adapter after both exist (e.g. an event-bus adapter receiving subscribers from services built later). Don't add the consumer to the adapter's Deps — that inverts the leaf rule, and constructor topo-ordering alone deadlocks on this shape.
Use construct-then-register inside NewComponents (forge disown internal/app/compose.go first to hand-own the construction site): build the adapter, build the consumer, then call the register/subscribe setter. It's an ordinary method call after both ends exist — not a framework seam.
bus := eventbus.New(eventbus.Deps{Logger: log})
svc := orders.New(orders.Deps{Bus: bus}) // consumer holds the adapter interface
bus.Subscribe("order.created", svc.OnOrderCreated) // phase two
There is no PostBootstrap / post_bootstrap.go seam — late registration is plain Go in the disowned compose.go. See the interactor skill for the canonical two-phase shape.
Scaffold an adapter with:
forge add adapter stripe-adapter
This emits the canonical package layout:
internal/stripe-adapter/
contract.go # // forge:adapter — Service interface, narrow surface
adapter.go # Service struct + downstream calls; New(Deps) Service
adapter_test.go # httptest stub of the downstream
Plus a cache.go stub for any local caching the adapter needs (delete it if you don't). forge add package <name> --type adapter resolves to the same code path.
Every adapter package's contract.go carries a // forge:adapter marker comment on the package doc. The marker tells the next reader the package's role in the architecture, and one lint rule enforces invariant:
forgeconv-adapter-no-rpc — adapter packages must not register Connect RPC handlers. Adapters are outbound-only; an RPC means it's actually a service.An adapter is a leaf built in internal/app/compose.go NewComponents (off the owned internal/app/providers.go Infra) and passed to consumers as an interface. No Setup(app *App), no name-matched App fields — just construct it and hand it down.
// internal/app/compose.go
stripe := stripeadapter.New(stripeadapter.Deps{
HTTPClient: &http.Client{Timeout: 30 * time.Second},
Cfg: infra.Cfg.Stripe, // scalars travel as one typed Config block
})
bill := billing.New(billing.Deps{Charges: stripe}) // consumer sees stripeadapter.Service, not the concrete type
Because the consumer depends on the interface, swapping the real adapter for a mock (tests) or a different backend is a one-line change here — the consumer is untouched.
interactor.service-layer.service-layer's errors section and forge/pkg/svcerr.forge add webhook.