Your First Schema
This guide takes you through building a more complete schema with object types, relationships, and services.
Understanding Effect Schema
Section titled “Understanding Effect Schema”Effect GraphQL uses Effect Schema as the single source of truth for your types. From one schema definition, you get:
- TypeScript types - Full type inference
- GraphQL types - Automatic conversion
- Validation - Runtime argument validation
import * as S from "effect/Schema"
// Define a schemaconst UserSchema = S.Struct({ id: S.String, name: S.String, age: S.Int.pipe(S.positive()), // Must be positive integer email: S.String.pipe(S.pattern(/@/)), // Must contain @})
// TypeScript type is inferredtype User = S.Schema.Type<typeof UserSchema>// { id: string; name: string; age: number; email: string }The Schema Builder
Section titled “The Schema Builder”GraphQLSchemaBuilder is an immutable builder for constructing your GraphQL schema:
import { GraphQLSchemaBuilder } from "@effect-gql/core"
const builder = GraphQLSchemaBuilder.empty .objectType({ name: "User", schema: UserSchema }) .query("hello", { type: S.String, resolve: () => Effect.succeed("world") })Each method returns a new builder instance (immutability). You can chain methods or use .pipe():
const schema = GraphQLSchemaBuilder.empty .objectType({ name: "User", schema: UserSchema }) .query("users", { ... }) .mutation("createUser", { ... }) .buildSchema()import { query, mutation, objectType } from "@effect-gql/core"
const schema = GraphQLSchemaBuilder.empty.pipe( objectType({ name: "User", schema: UserSchema }), query("users", { ... }), mutation("createUser", { ... }),).buildSchema()Defining Object Types
Section titled “Defining Object Types”Register object types with objectType():
const PostSchema = S.Struct({ id: S.String, title: S.String, body: S.String, authorId: S.String,})
const builder = GraphQLSchemaBuilder.empty .objectType({ name: "User", schema: UserSchema }) .objectType({ name: "Post", schema: PostSchema })Adding Queries and Mutations
Section titled “Adding Queries and Mutations”Simple Query
Section titled “Simple Query”.query("users", { type: S.Array(UserSchema), description: "Get all users", resolve: () => Effect.succeed(users),})Query with Arguments
Section titled “Query with Arguments”.query("user", { type: UserSchema, args: S.Struct({ id: S.String }), resolve: (args) => { const user = users.find(u => u.id === args.id) return user ? Effect.succeed(user) : Effect.fail(new NotFoundError({ message: "User not found" })) },})Mutation
Section titled “Mutation”.mutation("createUser", { type: UserSchema, args: S.Struct({ name: S.String.pipe(S.minLength(1)), email: S.String.pipe(S.pattern(/@/)), }), resolve: (args) => Effect.succeed({ id: generateId(), name: args.name, email: args.email, }),})Adding Computed Fields
Section titled “Adding Computed Fields”Add fields with custom resolvers using .field():
const builder = GraphQLSchemaBuilder.empty .objectType({ name: "User", schema: UserSchema }) .objectType({ name: "Post", schema: PostSchema }) // Add a computed field to User .field("User", "posts", { type: S.Array(PostSchema), resolve: (parent) => { // parent is typed as User const userPosts = posts.filter(p => p.authorId === parent.id) return Effect.succeed(userPosts) }, })Or define fields inline with objectType:
.objectType({ name: "User", schema: UserSchema, fields: { posts: { type: S.Array(PostSchema), resolve: (parent) => Effect.succeed( posts.filter(p => p.authorId === parent.id) ), }, fullName: { type: S.String, resolve: (parent) => Effect.succeed( `${parent.firstName} ${parent.lastName}` ), }, },})Complete Example
Section titled “Complete Example”Putting it all together:
import { Effect, Layer } from "effect"import * as S from "effect/Schema"import { GraphQLSchemaBuilder, execute } from "@effect-gql/core"
// Schemasconst UserSchema = S.Struct({ id: S.String, name: S.String, email: S.String,})
const PostSchema = S.Struct({ id: S.String, title: S.String, body: S.String, authorId: S.String,})
type User = S.Schema.Type<typeof UserSchema>type Post = S.Schema.Type<typeof PostSchema>
// Sample dataconst users: User[] = [ { id: "1", name: "Alice", email: "alice@example.com" }, { id: "2", name: "Bob", email: "bob@example.com" },]
const posts: Post[] = [ { id: "1", title: "Hello World", body: "...", authorId: "1" }, { id: "2", title: "GraphQL Tips", body: "...", authorId: "1" }, { id: "3", title: "Effect Patterns", body: "...", authorId: "2" },]
// Build schema with relationshipsconst schema = GraphQLSchemaBuilder.empty .objectType({ name: "User", schema: UserSchema }) .objectType({ name: "Post", schema: PostSchema })
// User.posts - posts by this user .field("User", "posts", { type: S.Array(PostSchema), resolve: (parent) => Effect.succeed( posts.filter(p => p.authorId === parent.id) ), })
// Post.author - the post's author .field("Post", "author", { type: UserSchema, resolve: (parent) => { const author = users.find(u => u.id === parent.authorId) return author ? Effect.succeed(author) : Effect.fail(new Error("Author not found")) }, })
// Queries .query("users", { type: S.Array(UserSchema), resolve: () => Effect.succeed(users), })
.query("posts", { type: S.Array(PostSchema), resolve: () => Effect.succeed(posts), })
.buildSchema()
// Execute a nested queryconst result = await Effect.runPromise( execute(schema, Layer.empty)(` query { users { name posts { title author { name } } } } `))
console.log(JSON.stringify(result, null, 2))Next Steps
Section titled “Next Steps”Now that you understand the basics:
- Learn about Effect-based resolvers and service injection
- Handle errors with structured error types
- Explore the Schema Builder API in depth