Skip to content

GraphQL

@kavo/graphql builds a GraphQLSchema over an existing createCrud service. Every resolver calls straight into the same engine REST uses: the same filter, sort, and pagination validation, the same error handling, and no parallel request path.

Zero-config mounting

Inside a Nest app, the fastest path is KavoModule's graphql option, which mounts a default controller merging every entity that registered its GraphQL types:

ts
KavoModule.forRoot({
  infrastructure: createInfrastructure(dataSource),
  graphql: true, // mounts POST /graphql
});

// Or choose the path:
KavoModule.forRoot({
  infrastructure: createInfrastructure(dataSource),
  graphql: { path: "api/graphql" },
});

Setting graphql implies provideServices, because the merged schema's resolvers need every entity's service as a DI provider to look them up.

Each entity registers its GraphQL types once, next to its other config. This is opt-in, not implied by @Kavo alone:

ts
// owner.graphql-types.ts
registerKavoGraphQLTypes(Owner, {
  itemType: OwnerType, // hand-written GraphQLObjectType
  createInputType: CreateOwnerInput, // optional — omit to skip the mutation
  updateInputType: UpdateOwnerInput,
  patchInputType: PatchOwnerInput,
  deleteOne: true,
  restoreOne: true, // meaningful only if Owner declared soft delete
  purgeOne: true,
});

Each field is opt-in per entity. Omitting an option leaves the field out of the schema entirely:

FieldEnabled by
Query.owner(id)always
Query.owners(limit, offset, sort, filter)always
Mutation.createOwnercreateInputType
Mutation.updateOwnerupdateInputType
Mutation.patchOwnerpatchInputType
Mutation.deleteOwner: BooleandeleteOne: true
Mutation.restoreOwner: OwnerrestoreOne: true
Mutation.purgeOwner: BooleanpurgeOne: true

filter and sort on Query.owners use Kavo's own grammar, not a generated per-entity input type. sort takes REST's -field string convention. filter takes a raw filter-AST JSON scalar ({ kind: "condition", field, operator, value }, operators in SCREAMING_SNAKE) rather than a typed input object.

Custom operations

A custom operation reaches the schema too, opt-in per id through the same operations option:

ts
registerKavoGraphQLTypes(Order, {
  itemType: OrderType,
  operations: {
    markPaidOne: { type: OrderType }, // { inputType } too, for a write that takes a body
  },
});

Naming an id there is not enough by itself: the operation still has to be enabled and declare a matching schema shape (operations.markPaidOne.schema.output, and schema.input if inputType is given) on the entity's own config — a custom id has no entity-derived schema fallback the way the standard eight do, so there is nothing to build a typed field from otherwise. Naming an id here whose registry entry is missing, disabled, or missing the matching declared shape fails at schema-build time with a ConfigurationException, not a silently omitted field.

The field's placement — Query or Mutation — follows the operation's registered kind ("read"/"write"), the same as everywhere else the registry decides that. A cardinality-"one" operation takes an id argument the way update/delete/etc. do; a cardinality-"many" one does not. The field name is <lowerName><OperationId> (orderMarkPaidOne), namespaced by the entity the same way the standard fields already are.

Mounting your own controller

For more control (a custom path, guards, interceptors) extend BaseKavoGraphQLController instead of using the graphql option:

ts
@Controller("graphql")
export class GraphQLController extends BaseKavoGraphQLController {
  constructor(moduleRef: ModuleRef) {
    super(moduleRef);
  }

  @Post()
  @HttpCode(200) // GraphQL-over-HTTP convention: 200 even for a mutation
  @UseGuards(GraphQLAuthGuard)
  handle(@Body() body: { query: string; variables?: Record<string, unknown> }) {
    return this.execute(body.query, body.variables);
  }
}

Pick one mounting approach per app. The zero-config option and a hand-written controller are alternatives, never both at the same path.

Outside Nest

@kavo/graphql is host-framework-agnostic: it imports only @kavo/core and the graphql peer, never @kavo/nest. createKavoGraphQLSchema and mergeKavoGraphQLSchemas build a schema directly from one or more createCrud services, for any host that can serve a GraphQLSchema over HTTP.

Installing it

graphql is an optional peer of both @kavo/nest and @kavo/graphql itself, so a REST-only install pulls in neither.

Inside a Nest app, @kavo/nest already depends on @kavo/graphql. Add just the peer:

bash
pnpm add graphql
bash
npm install graphql
bash
yarn add graphql
bash
bun add graphql

Outside Nest, add @kavo/graphql yourself too, alongside @kavo/core and whichever ORM adapter you use:

bash
pnpm add @kavo/core @kavo/graphql graphql
bash
npm install @kavo/core @kavo/graphql graphql
bash
yarn add @kavo/core @kavo/graphql graphql
bash
bun add @kavo/core @kavo/graphql graphql

See Peer dependencies for the full version table.

What's not covered yet

  • Relations and includes aren't exposed as GraphQL fields. Only scalar itemType fields exist today; a relation would need to be hand-added with its own resolver.
  • There's no generated per-entity FilterInput type.
  • The list envelope's meta bag doesn't reach GraphQL clients. The generated list type declares items, total, limit, and offset only.

See GraphQL binding for the full design, including the one-directional frameworks/* → protocols/* package boundary (ADR-0016) that lets @kavo/nest depend on this package without the reverse ever being true.