Skip to content

Entrypoints

Intent

A service may need:

  • GraphQL for product-facing consumption
  • HTTP for internal or operational endpoints
  • Future transports without rewriting the use case

The same command or query can be invoked from any entrypoint. Only transport-specific mapping differs.

Flow

Example

The handler is shared. Entrypoints call execute on the module handler from the request container.

ts
// shell/cqrs/queries/presence.query.ts
export class PresenceQuery extends Query<PresenceQuery.Input, PresenceQuery.Output> {
	// ...
	execute = async (input: PresenceQuery.Input): PresenceQuery.Output => {
		return await this.presenceRepository.findByCardId(input.cardId);
	};
}

GraphQL

ts
const presence = await ctx.container.seasonCard.presence.execute({
	cardId: seasonCard.id,
});

return presence;

Domain errors thrown inside the handler bubble up and are handled by the GraphQL format-error middleware in @repo/core-server. No per-resolver catch required.

HTTP

ts
await request.container.seasonCard.simulatePresence.execute({
	cardId: request.query.cardId,
	renewalStatus: request.query.renewalStatus,
	rewardStatus: request.query.rewardStatus,
});

return reply.status(200).send({ success: true });

Domain errors thrown inside the handler bubble up and are handled by the Fastify error middleware in @repo/core-server as RFC 9457 problem+json responses. No per-route catch required.

When a use case returns client errors (e.g. validation failures), map them in the entrypoint. See Errors.

Design rule

If two entrypoints need different behavior, the difference should usually live in mapping or transport handling, not in duplicated business logic.