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:
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:
| Method | What 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). |
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:
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:
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,
});
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 theObjectTypesinterface.args— the field arguments, typed from the schema.ctx— the request context. See Context.info— theGraphQLResolveInfofor 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,
});
Override source types via the ObjectTypes interface when your data layer's shape diverges from the GraphQL schema.
Throw typed errors from @baeta/errors so clients can branch on extensions.code instead of message strings.