Resolvers
Resolvers in Effect GraphQL are Effect programs. This gives you type-safe error handling, dependency injection, and composability out of the box.
Basic Resolvers
Section titled “Basic Resolvers”A resolver is a function that returns an Effect:
import { Effect } from "effect"import * as S from "effect/Schema"
.query("hello", { type: S.String, resolve: () => Effect.succeed("world"),})With Arguments
Section titled “With Arguments”Arguments are passed to the resolver function and are automatically validated:
.query("greet", { type: S.String, args: S.Struct({ name: S.String }), resolve: (args) => Effect.succeed(`Hello, ${args.name}!`),})The args object is fully typed based on your schema.
Field Resolvers
Section titled “Field Resolvers”Field resolvers on object types receive the parent value:
.field("User", "fullName", { type: S.String, resolve: (parent) => Effect.succeed( `${parent.firstName} ${parent.lastName}` ),})With arguments:
.field("User", "posts", { type: S.Array(PostSchema), args: S.Struct({ limit: S.optional(S.Int), offset: S.optional(S.Int), }), resolve: (parent, args) => Effect.succeed( posts .filter(p => p.authorId === parent.id) .slice(args.offset ?? 0, args.limit) ),})Service Injection
Section titled “Service Injection”The real power of Effect resolvers is service injection. Define services with Effect’s Context.Tag:
import { Context, Layer } from "effect"
// Define a service interfaceclass UserService extends Context.Tag("UserService")< UserService, { readonly getAll: () => Effect.Effect<User[]> readonly getById: (id: string) => Effect.Effect<User | null> readonly create: (name: string, email: string) => Effect.Effect<User> }>() {}Use services in resolvers with Effect.gen:
.query("users", { type: S.Array(UserSchema), resolve: () => Effect.gen(function* () { const userService = yield* UserService return yield* userService.getAll() }),})
.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) }),})Providing Services
Section titled “Providing Services”Services are provided when executing queries via a Layer:
import { execute } from "@effect-gql/core"
// Create service implementationconst UserServiceLive = Layer.succeed(UserService, { getAll: () => Effect.succeed(users), getById: (id) => Effect.succeed(users.find(u => u.id === id) ?? null), create: (name, email) => Effect.succeed({ id: generateId(), name, email }),})
// Compose all servicesconst AppLayer = Layer.merge(UserServiceLive, PostServiceLive)
// Execute with servicesconst result = await Effect.runPromise( execute(schema, AppLayer)(` query { users { id name } } `))Effect Composition
Section titled “Effect Composition”Since resolvers are Effects, you can use all Effect combinators:
Sequential Operations
Section titled “Sequential Operations”resolve: (args) => Effect.gen(function* () { const userService = yield* UserService const user = yield* userService.getById(args.id)
if (!user) { return yield* Effect.fail(new NotFoundError({ message: "User not found" })) }
const postService = yield* PostService const posts = yield* postService.getByAuthor(user.id)
return { ...user, posts }})Parallel Operations
Section titled “Parallel Operations”resolve: () => Effect.gen(function* () { const userService = yield* UserService const postService = yield* PostService
// Fetch in parallel const [users, posts] = yield* Effect.all([ userService.getAll(), postService.getAll(), ], { concurrency: "unbounded" })
return { users, posts }})Error Handling
Section titled “Error Handling”resolve: (args) => Effect.gen(function* () { const userService = yield* UserService const user = yield* userService.getById(args.id)
if (!user) { return yield* Effect.fail( new NotFoundError({ message: `User ${args.id} not found` }) ) }
return user}).pipe( Effect.catchTag("DatabaseError", (e) => Effect.fail(new GraphQLError({ message: "Database unavailable" })) ))The Execute Function
Section titled “The Execute Function”The execute function bridges Effect resolvers with GraphQL execution:
import { execute } from "@effect-gql/core"
const runQuery = execute(schema, serviceLayer)
// Execute a queryconst result = await Effect.runPromise( runQuery( `query GetUser($id: String!) { user(id: $id) { name } }`, { id: "123" }, // variables "GetUser" // operation name (optional) ))How It Works
Section titled “How It Works”- Creates an Effect runtime from your Layer
- Passes the runtime to GraphQL via context
- Each resolver receives the runtime and runs its Effect
- Services are available throughout the request
// Simplified internal flowexecute(schema, layer)(query, variables) => Effect.gen(function* () { const runtime = yield* Effect.runtime<R>() const result = yield* Effect.promise(() => graphql({ schema, source: query, variableValues: variables, contextValue: { runtime }, }) ) return result }).pipe(Effect.provide(layer))Request Context
Section titled “Request Context”Access request-scoped data using GraphQLRequestContext:
import { GraphQLRequestContext, makeRequestContextLayer } from "@effect-gql/core"
.query("me", { type: UserSchema, resolve: () => Effect.gen(function* () { const ctx = yield* GraphQLRequestContext const authHeader = ctx.request.headers["authorization"]
// Extract user from token... const userId = parseToken(authHeader)
const userService = yield* UserService return yield* userService.getById(userId) }),})Create the request context layer at request time:
const requestLayer = makeRequestContextLayer({ request: { headers: req.headers, query: req.body.query, variables: req.body.variables, },})
const fullLayer = Layer.merge(requestLayer, AppLayer)
execute(schema, fullLayer)(query, variables)Type Safety
Section titled “Type Safety”The builder tracks service requirements in its type parameter:
// Type: GraphQLSchemaBuilder<never>const empty = GraphQLSchemaBuilder.empty
// Type: GraphQLSchemaBuilder<UserService>const withUserQuery = empty.query("users", { type: S.Array(UserSchema), resolve: () => Effect.gen(function* () { const userService = yield* UserService return yield* userService.getAll() }),})
// Type: GraphQLSchemaBuilder<UserService | PostService>const withBoth = withUserQuery.query("posts", { type: S.Array(PostSchema), resolve: () => Effect.gen(function* () { const postService = yield* PostService return yield* postService.getAll() }),})When you call execute(schema, layer), TypeScript verifies that layer provides all required services.