Skip to content

ADR-0016 — GraphQL is a protocols/ package, one-directional from frameworks/

Status: accepted

Context

ADR-0002 established two parent folders — orms/ (adapters) and frameworks/ (framework bindings) — and said no speculative packages get created beyond what a real feature needs. Adding GraphQL support raised a question ADR-0002 never had to answer: GraphQL is not an ORM adapter, and it is not quite a "framework binding" in the sense @kavo/nest is one. @kavo/nest bundles two things together — a host framework (Nest's DI, routing, decorators) and a wire protocol (REST, via decoration-time route generation). GraphQL is only the second thing: a wire protocol/API paradigm that could sit atop any host framework — Nest today, a future Express/Fastify/Next.js binding later — the same way REST does. Treating @kavo/graphql as a third frameworks/* sibling would conflate protocol with host framework and give it no natural way to reach @kavo/nest's discovery internals (getKavoEntities(), ModuleRef) without either package importing the other in both directions — exactly the frameworks/* <-> frameworks/* edge ADR-0002's boundary rules exist to prevent between adapter and framework.

Decision

A new parent folder, packages/protocols/, holds wire-protocol bindings that are host-framework-agnostic: @kavo/graphql is the first. These packages depend on @kavo/core only (dependency-cruiser: protocol-bindings-only-import-core) — exactly like an ORM adapter's constraint, never a host framework package.

Host framework packages (packages/frameworks/*) may depend on a protocol package to build framework-specific glue — @kavo/nest depends on @kavo/graphql to provide BaseKavoGraphQLController and createDefaultGraphQLController — but never the reverse. protocol-bindings-only-import-core enforces the reverse direction by blocking @kavo/graphql from importing packages/frameworks or @kavo/nest. The resulting dependency direction is frameworks/* → protocols/*, mirroring frameworks/* → core and orms/* → core: protocols and ORMs are both leaves adapting one concern; framework packages are the only ones allowed to depend sideways, and only toward protocols — adapters still meet frameworks only through Nest's DI container, unchanged from ADR-0002.

Cases:

  • @kavo/graphql may depend on @kavo/core and the graphql peer only.
  • @kavo/nest may depend on @kavo/core, its @nestjs/* peers, and @kavo/graphql — never an ORM adapter (framework-bindings-import-core-and-protocol-barrels unchanged).
  • A future @kavo/grpc follows the protocols/ shape above. A future @kavo/express/@kavo/fastify/@kavo/nextjs follows the frameworks/ shape and may depend on whichever protocols/* package(s) it wants to offer glue for.

An entity opts into GraphQL exposure separately from its @Kavo config — registerKavoGraphQLTypes(Entity, {...}), not a @Kavo(Entity, { graphql: {...} }) option — because @kavo/graphql cannot know @Kavo exists (same reasoning ADR-0002 already gives for adapters never being reachable from framework config directly).

Consequences

  • @kavo/graphql's host-agnostic discovery helper (resolveKavoGraphQLSchema) is reusable verbatim by any future host framework binding — only "how to enumerate entities" and "how to resolve a bound service" differ per host, and both are supplied by the caller, never by @kavo/graphql itself.
  • Two framework bindings for the same protocol (e.g. a future Nest and Express GraphQL glue) share code only through @kavo/graphql, never with each other directly — the frameworks/* <-> frameworks/* edge stays forbidden.
  • ADR-0002's "no speculative packages" still holds: protocols/graphql is real, built, tested code, not a stub — a future protocols/grpc gets created when there is real work to put in it, not preemptively.