Skip to main content
Version: Next (2.x)

Resolvers

Resolvers in Baeta are chained method calls on the generated module: declare them once per field with .resolve() / .map() / .key(), then collect them with $fields. This page walks through a User type and its Query together.

Project structure

src/modules/
└── user/
├── index.ts # exports the module
├── user.gql # schema definition for the module
├── query.resolver.ts # field resolvers for the Query type in the module
├── user.resolver.ts # field resolvers for the User type in the module
└── typedef.ts # auto-generated types for the module

Schema definition

src/modules/user/user.gql:

type User {
id: ID!
email: String!
lastName: String!
profile: String
givenName: String
birthDate: DateTime!
friends: [User!]!
}

input UserWhereUniqueInput {
id: ID!
}

type Query {
user(where: UserWhereUniqueInput!): User
users: [User!]
}

Implement the resolvers

Define the field resolvers in src/modules/user/user.resolver.ts:

src/modules/user/user.resolver.ts
import { UserModule } from "./typedef.ts";

const { User } = UserModule;

const toDate = (str: string) => new Date(str);

const userFriends = User.friends.map(async ({ source }) => {
return await getFriends(source.id);
});

export default User.$fields({
id: User.id.key("id"),
email: User.email.key("email"),
lastName: User.lastName.key("lastName"),
profile: User.profile.key("profile"),
givenName: User.givenName.key("givenName"),
birthDate: User.birthDate.key("birthDate").to(toDate),
friends: userFriends.withDefault([]),
});

Field resolvers expose a small set of chainable helpers. Pick the strictest one that fits:

MethodWhat it does
.resolve(fn)Type-checked resolver — return type must match the schema field. Async-friendly.
.map(fn)Same shape as .resolve, but accepts any return type. Use for chained transforms that converge to the right type.
.key("name")Shorthand for .map(({ source }) => source.name).
.to(fn)Transforms whatever the previous step produced.
.withDefault(value)Substitutes value when the result is null/undefined.
.undefinedAsNull()Maps undefined to null (useful for nullable fields).
tip

These methods compose freely. If the composition produces the wrong type for the field, the error surfaces when you pass the resolver into User.$fields({…}).

Query resolvers work the same way:

src/modules/user/query.resolver.ts
import { UserModule } from "./typedef.ts";

const { Query } = UserModule;

const userQuery = Query.user.map(({ args, ctx }) => {
return ctx.dataSources.users.findUnique(args.where);
});

const usersQuery = Query.users.resolve(({ ctx }) => {
return ctx.dataSources.users.findMany();
});

export default Query.$fields({
user: userQuery,
users: usersQuery,
});

Finally, compose the module resolvers in src/modules/user/index.ts:

src/modules/user/index.ts
import { UserModule } from "./typedef.ts";
import queryResolver from "./query.resolver.ts";
import userResolver from "./user.resolver.ts";

export default UserModule.$schema({
Query: queryResolver,
User: userResolver,
});
tip

The module will be automatically imported in src/modules/index.ts.

Resolver parameters

Every resolver receives a single argument with four properties:

  • source — the parent object. Customize its type via the ObjectTypes interface.
  • args — the field arguments, typed from the schema.
  • ctx — the request context. See Context.
  • info — the GraphQLResolveInfo for the field.

Extending a type from another module

src/modules/user-extended/user-posts.gql:

extend type User {
fullName: String!
posts: [Post!]
}

Resolve the extended field in the new module — src/modules/user-posts/resolvers.ts:

import { UserExtendedModule } from "./typedef.ts";

const { User } = UserExtendedModule;

const userPosts = User.posts.resolve(({ source, ctx }) => {
return ctx.db.posts.findMany({
where: { authorId: source.id },
});
});

export default User.$fields({
posts: userPosts,
});
tip

Override source types via the ObjectTypes interface when your data layer's shape diverges from the GraphQL schema.

tip

Throw typed errors from @baeta/errors so clients can branch on extensions.code instead of message strings.