Skip to content

Schema Builder

The GraphQLSchemaBuilder is the core API for constructing GraphQL schemas in Effect GraphQL. It provides an immutable, pipeable interface that accumulates type information as you build.

import { GraphQLSchemaBuilder } from "@effect-gql/core"
// Start with an empty builder
const builder = GraphQLSchemaBuilder.empty
// Build your schema
const schema = builder
.query("hello", { type: S.String, resolve: () => Effect.succeed("world") })
.buildSchema()

Every method returns a new builder instance. This enables safe composition:

const base = GraphQLSchemaBuilder.empty
.objectType({ name: "User", schema: UserSchema })
// Create different schemas from the same base
const simpleSchema = base
.query("users", { ... })
.buildSchema()
const fullSchema = base
.query("users", { ... })
.mutation("createUser", { ... })
.buildSchema()

For a more functional style, use the standalone functions with .pipe():

import {
query,
mutation,
objectType,
field,
compose,
} from "@effect-gql/core"
const schema = GraphQLSchemaBuilder.empty.pipe(
objectType({ name: "User", schema: UserSchema }),
query("users", { type: S.Array(UserSchema), resolve: () => Effect.succeed([]) }),
mutation("createUser", { type: UserSchema, args: CreateUserInput, resolve: ... }),
).buildSchema()

Use compose to combine multiple operations:

const withUserTypes = compose(
objectType({ name: "User", schema: UserSchema }),
objectType({ name: "Post", schema: PostSchema }),
)
const withQueries = compose(
query("users", { ... }),
query("posts", { ... }),
)
const schema = GraphQLSchemaBuilder.empty.pipe(
withUserTypes,
withQueries,
).buildSchema()
const UserSchema = S.Struct({
id: S.String,
name: S.String,
email: S.String,
})
builder.objectType({ name: "User", schema: UserSchema })

With S.TaggedStruct or S.Class, names are inferred automatically:

const UserSchema = S.TaggedStruct("User", {
id: S.String,
name: S.String,
})
// Name "User" is inferred
builder.objectType({ schema: UserSchema })

Add fields with resolvers inline:

builder.objectType({
name: "User",
schema: UserSchema,
fields: {
fullName: {
type: S.String,
resolve: (parent) => Effect.succeed(
`${parent.firstName} ${parent.lastName}`
),
},
posts: {
type: S.Array(PostSchema),
args: S.Struct({ limit: S.optional(S.Int) }),
resolve: (parent, args) => Effect.gen(function* () {
const postService = yield* PostService
return yield* postService.getByAuthor(parent.id, args.limit)
}),
},
},
})

Use .field() to add fields after type registration:

builder
.objectType({ name: "User", schema: UserSchema })
.field("User", "posts", {
type: S.Array(PostSchema),
resolve: (parent) => Effect.succeed([]),
})
// Simple query
.query("hello", {
type: S.String,
resolve: () => Effect.succeed("world"),
})
// With arguments
.query("user", {
type: UserSchema,
args: S.Struct({ id: S.String }),
resolve: (args) => Effect.succeed({ id: args.id, name: "Alice" }),
})
// With description
.query("users", {
type: S.Array(UserSchema),
description: "Get all users in the system",
resolve: () => Effect.succeed([]),
})
.mutation("createUser", {
type: UserSchema,
args: S.Struct({
name: S.String,
email: S.String,
}),
resolve: (args) => Effect.gen(function* () {
const userService = yield* UserService
return yield* userService.create(args.name, args.email)
}),
})

Subscriptions return an Effect that produces a Stream:

import { Stream } from "effect"
.subscription("userCreated", {
type: UserSchema,
subscribe: () => Effect.gen(function* () {
const events = yield* EventService
return events.userCreatedStream()
}),
})

Register enum types with values:

.enumType({
name: "UserStatus",
values: ["ACTIVE", "INACTIVE", "PENDING"],
description: "User account status",
})

Use in queries with S.Literal:

.query("usersByStatus", {
type: S.Array(UserSchema),
args: S.Struct({
status: S.Literal("ACTIVE", "INACTIVE", "PENDING"),
}),
resolve: (args) => Effect.succeed([]),
})
const NodeSchema = S.Struct({ id: S.String })
builder
.interfaceType({ name: "Node", schema: NodeSchema })
.objectType({
name: "User",
schema: UserSchema,
implements: ["Node"],
})
const TextSchema = S.TaggedStruct("Text", { body: S.String })
const ImageSchema = S.TaggedStruct("Image", { url: S.String })
builder
.objectType({ schema: TextSchema })
.objectType({ schema: ImageSchema })
.unionType({
name: "Content",
types: ["Text", "Image"],
})

Register input types for complex arguments:

const CreateUserInput = S.Struct({
name: S.String,
email: S.String,
role: S.optional(S.Literal("USER", "ADMIN")),
})
builder
.inputType({ name: "CreateUserInput", schema: CreateUserInput })
.mutation("createUser", {
type: UserSchema,
args: S.Struct({ input: CreateUserInput }),
resolve: (args) => Effect.succeed({ id: "1", ...args.input }),
})

Register custom directives:

import { DirectiveLocation } from "graphql"
builder.directive({
name: "auth",
description: "Requires authentication",
locations: [DirectiveLocation.FIELD_DEFINITION],
args: S.Struct({
role: S.optional(S.Literal("USER", "ADMIN")),
}),
transformer: (effect, args) =>
Effect.gen(function* () {
const auth = yield* AuthService
yield* auth.requireRole(args.role ?? "USER")
return yield* effect
}),
})

Call .buildSchema() to produce the final GraphQLSchema:

const schema = builder.buildSchema()
// Use with any GraphQL server
import { graphql } from "graphql"
const result = await graphql({ schema, source: "{ hello }" })