Skip to content

GraphQLSchemaBuilder

The GraphQLSchemaBuilder is an immutable, pipeable builder for constructing GraphQL schemas. It accumulates type definitions and resolver registrations, then builds a standard GraphQLSchema.

import { GraphQLSchemaBuilder } from "@effect-gql/core"

Creates a new empty schema builder.

const builder = GraphQLSchemaBuilder.empty
// Type: GraphQLSchemaBuilder<never>

The type parameter R tracks accumulated service requirements from resolvers.

Register a Query field.

builder.query("users", {
type: S.Array(UserSchema),
args?: { ... },
description?: "...",
deprecationReason?: "...",
resolve: (args) => Effect.succeed([...])
})

Parameters:

NameTypeDescription
namestringField name
config.typeSchema<A>Return type schema
config.argsRecord<string, Schema>Optional arguments
config.descriptionstringOptional field description
config.deprecationReasonstringMark field as deprecated
config.resolve(args) => Effect<A, E, R>Resolver function

Returns: GraphQLSchemaBuilder<R | R2> (accumulates service requirements)

Register a Mutation field. Same signature as query().

builder.mutation("createUser", {
type: UserSchema,
args: { name: S.String, email: S.String },
resolve: ({ name, email }) => Effect.gen(function* () {
const db = yield* Database
return yield* db.createUser({ name, email })
})
})

Register a Subscription field. The resolver must return a Stream.

builder.subscription("messageAdded", {
type: MessageSchema,
args: { channelId: S.String },
resolve: ({ channelId }) => Effect.gen(function* () {
const pubsub = yield* PubSubService
return pubsub.subscribe(`channel:${channelId}`)
})
})

Returns: The resolver must return Effect<Stream<A>, E, R>.

Register a GraphQL Object type.

builder.objectType({
name: "User", // Required for plain structs
schema: UserSchema,
description?: "..."
})
// Name is optional for TaggedStruct/TaggedClass/Class
builder.objectType({
schema: S.TaggedStruct("User", { id: S.String, name: S.String })
})

Type Name Inference:

  • S.TaggedStruct("Name", {...}) → Name extracted from tag
  • S.TaggedClass("Name") → Name extracted from tag
  • S.Class("Name") → Name extracted from class
  • Plain S.Struct({...}) → Requires explicit name parameter

Register a GraphQL Interface type.

builder.interfaceType({
name: "Node",
schema: NodeSchema,
description?: "..."
})

Register a GraphQL Union type.

builder.unionType({
name: "SearchResult",
types: ["User", "Post", "Comment"],
description?: "...",
resolveType?: (value) => {
// Return the type name based on the value
if ("email" in value) return "User"
if ("title" in value) return "Post"
return "Comment"
}
})

Register a GraphQL Enum type.

builder.enumType({
name: "Status",
values: ["ACTIVE", "INACTIVE", "PENDING"],
description?: "..."
})
// Or with descriptions per value
builder.enumType({
name: "Status",
values: {
ACTIVE: { description: "Currently active" },
INACTIVE: { description: "Disabled" },
PENDING: { description: "Awaiting approval" }
}
})

Register a GraphQL Input type for complex arguments.

builder.inputType({
name: "CreateUserInput",
schema: CreateUserInputSchema,
description?: "..."
})

Add a computed or relational field to an object type.

builder.field("User", "posts", {
type: S.Array(PostSchema),
args?: { limit: S.optional(S.Number) },
description?: "...",
resolve: (parent, args) => Effect.gen(function* () {
const db = yield* Database
return yield* db.getPostsByUserId(parent.id, args.limit)
})
})

Parameters:

NameTypeDescription
typeNamestringObject type to add field to
fieldNamestringField name
config.typeSchema<A>Return type schema
config.argsRecord<string, Schema>Optional arguments
config.resolve(parent, args) => Effect<A>Resolver receiving parent value

Register a custom directive with optional resolver transformation.

builder.directive("auth", {
locations: ["FIELD_DEFINITION"],
args: { role: S.optional(S.String) },
description: "Require authentication",
transformer: (next, args, info) => Effect.gen(function* () {
const ctx = yield* AuthContext
if (args.role && ctx.user.role !== args.role) {
return yield* Effect.fail(new AuthorizationError({
message: "Insufficient permissions"
}))
}
return yield* next
})
})

Parameters:

NameTypeDescription
namestringDirective name (without @)
config.locationsDirectiveLocation[]Where directive can be used
config.argsRecord<string, Schema>Directive arguments
config.descriptionstringDirective description
config.transformer(next, args, info) => EffectResolver wrapper

Build the final GraphQL schema.

const schema: GraphQLSchema = builder.buildSchema()

Returns: Standard GraphQLSchema from the graphql package.

Throws: If the schema is invalid (e.g., missing required types).

The builder implements Pipeable for fluent composition:

import { GraphQLSchemaBuilder, query, objectType, field } from "@effect-gql/core"
const builder = GraphQLSchemaBuilder.empty.pipe(
query("users", { ... }),
objectType({ name: "User", schema: UserSchema }),
field("User", "posts", { ... })
)

See Pipe Functions for all available functions.

The builder tracks service requirements via its type parameter:

const builder: GraphQLSchemaBuilder<Database | AuthService> =
GraphQLSchemaBuilder.empty.pipe(
query("users", {
type: S.Array(UserSchema),
resolve: () => Effect.gen(function* () {
const db = yield* Database // Adds Database to R
const auth = yield* AuthService // Adds AuthService to R
// ...
})
})
)

This ensures you provide all required services when executing queries.

import { GraphQLSchemaBuilder, query, mutation, objectType, field } from "@effect-gql/core"
import { Effect, Context, Layer } from "effect"
import * as S from "effect/Schema"
// Define schemas
const UserSchema = S.Struct({
id: S.String,
name: S.String,
email: S.String
})
const PostSchema = S.Struct({
id: S.String,
title: S.String,
authorId: S.String
})
// Build schema
const builder = GraphQLSchemaBuilder.empty.pipe(
// Register types
objectType({ name: "User", schema: UserSchema }),
objectType({ name: "Post", schema: PostSchema }),
// Root operations
query("users", {
type: S.Array(UserSchema),
resolve: () => Effect.succeed([
{ id: "1", name: "Alice", email: "alice@example.com" }
])
}),
query("user", {
type: S.NullOr(UserSchema),
args: { id: S.String },
resolve: ({ id }) => Effect.succeed(
id === "1" ? { id: "1", name: "Alice", email: "alice@example.com" } : null
)
}),
// Relational fields
field("User", "posts", {
type: S.Array(PostSchema),
resolve: (user) => Effect.succeed([
{ id: "1", title: "Hello World", authorId: user.id }
])
}),
field("Post", "author", {
type: UserSchema,
resolve: (post) => Effect.succeed({
id: post.authorId,
name: "Alice",
email: "alice@example.com"
})
})
)
// Build and use
const schema = builder.buildSchema()