GraphQL Wiring
Recipes are working wiring examples with the sharp edges annotated. This page and the
graphql-wiringrecipe served to coding agents by the MCP server share one source (@modularityjs/manifest), so they never drift apart.
Schema-first: the app owns the SDL, resolver classes bind methods to schema fields. Two packages, same split as HTTP:
GraphqlModule(contract) — ownsGraphqlTypeDefsPool(value pool of SDL fragments),GraphqlResolversPool(class pool),GraphqlConfig(pathdefault/graphql,graphiqldefault on outside production,introspectiondefaulttrue). Decorators:@Resolver,@Query,@Mutation,@ResolveField,@Args,@Parent,@Context.HttpFastifyGraphqlModule(extension) — composes the SDL fragments, validates every decorated resolver method against the built schema inafterLoad, and mounts a mercurius endpoint atGraphqlConfig.path. RequiresHttpFastifyModule(it reaches into the same Fastify instance).
Exactly one SDL fragment defines the base type Query (and type Mutation if used); every other fragment uses extend type Query. Fragment order never matters — graphql-js applies extend type nodes document-wide.
Unlike the WebSocket pool, wiring mistakes fail boot loudly: a pooled class without @Resolver(), a @Query() method mapping to a field the schema doesn't declare, a schema Query/Mutation field with no resolver method, or two methods claiming the same field all abort startup with a message naming the class, the method, and the fix. Plain object-type fields (e.g. User.name served straight off the returned object) need no resolver — graphql default resolvers cover them; @ResolveField is for computed/lazy fields.
import { Inject, Injectable } from '@modularityjs/di';
import { NotFoundException } from '@modularityjs/exception';
import type { GraphqlContext } from '@modularityjs/graphql';
import {
Args,
Context,
GraphqlModule,
GraphqlResolversPool,
GraphqlTypeDefsPool,
Mutation,
Parent,
Query,
ResolveField,
Resolver,
} from '@modularityjs/graphql';
import { HttpModule } from '@modularityjs/http';
import { HttpFastifyModule } from '@modularityjs/http-fastify';
import { HttpFastifyGraphqlModule } from '@modularityjs/http-fastify-graphql';
import { Module } from '@modularityjs/modularity';
interface User {
id: string;
name: string;
}
const USERS_TYPE_DEFS = `
type Query {
user(id: ID!): User
}
type Mutation {
createUser(name: String!): User!
}
type User {
id: ID!
name: String!
greeting: String!
}
`;
@Injectable()
class UserService {
private readonly users = new Map<string, User>([
['1', { id: '1', name: 'Ada' }],
]);
find(id: string): User {
const user = this.users.get(id);
if (!user) throw new NotFoundException(`User "${id}" not found.`);
return user;
}
create(name: string): User {
const user = { id: String(this.users.size + 1), name };
this.users.set(user.id, user);
return user;
}
}
@Resolver('User') // type name required only because of @ResolveField below
class UserResolver {
constructor(@Inject(UserService) private readonly users: UserService) {}
@Query('user') // maps to Query.user; @Query() would default to the method name
user(@Args('id') id: string): User {
return this.users.find(id); // NotFoundException -> errors[0].extensions.code === 'NOT_FOUND'
}
@Mutation()
createUser(
@Args('name') name: string,
@Context() context: GraphqlContext,
): User {
void context.identity; // identity populated by http-auth when wired, else undefined
return this.users.create(name);
}
@ResolveField() // maps to User.greeting, receives the parent User
greeting(@Parent() user: User): string {
return `Hello, ${user.name}!`;
}
}
@Module({
name: 'users',
imports: [GraphqlModule],
providers: [UserService, UserResolver],
pools: [
{ pool: GraphqlTypeDefsPool, key: 'users', useValue: USERS_TYPE_DEFS }, // value pool
{ pool: GraphqlResolversPool, key: 'users', useClass: UserResolver }, // class pool
],
})
class UsersModule {}
// A second feature module contributes its slice with `extend`:
// { pool: GraphqlTypeDefsPool, key: 'billing', useValue: 'extend type Query { invoices: [String!]! }' }
// modules: [
// ModularityModule,
// HttpModule.forRoot({ port: 3000 }),
// HttpFastifyModule, // required — the GraphQL extension reuses the Fastify instance
// GraphqlModule, // or GraphqlModule.forRoot({ path: '/api/graphql', graphiql: false })
// HttpFastifyGraphqlModule,
// UsersModule,
// ]Resolver exceptions never leak internals: a thrown @modularityjs/exception subclass surfaces as a GraphQL error carrying its stable extensions.code (plus errors for ValidationException, context when set); any other throw is masked as "Internal server error" / INTERNAL_SERVER_ERROR and reported via process.emitWarning (ModularityJsGraphqlResolverError).
@Args('input', schema) accepts an optional Schema<T> from @modularityjs/validation-schema (a primitive — no module wiring needed): the value is parsed before the method runs, and a failed parse becomes a VALIDATION_FAILED error with extensions.errors. @Args() without a name injects the whole arguments object.
When http-auth is wired, its onRequest hook runs before the GraphQL handler, so @Context() exposes the caller as context.identity ({ id, attributes }) with zero extra configuration — the GraphQL packages do not depend on http-auth.
GraphiQL is served at /graphiql when GraphqlConfig.graphiql is on (default: only outside production) and requires introspection (config validation rejects the combination graphiql: true, introspection: false). The endpoint is mounted outside the @Controller pipeline — it does not appear in HttpServer.getRoutes() or OpenAPI, same as the /mcp endpoint. Subscriptions are not supported yet — @Query/@Mutation/@ResolveField only.