Resolvers
Baeta provides a modular, type-safe approach to defining GraphQL resolvers. Let's look at how to implement User and Query resolvers.
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
First, define your types and queries in 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 Resolvers
After defining your schema, implement the User 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([]),
});
Each field resolver comes with chainable methods that allow you to:
map(({ source, args, ctx, info }) => any value):- Use resolver params to return a new value.
- It accepts any value as return type.
- Can be async.
resolve(({ source, args, ctx, info }) => expected value):- Identical to
.mapbut enforces the return type to be of the expected value. - Can be async.
- Identical to
key(string):- Return the field value from the source object.
- It's a shortcut for
.map(({ source }) => source[key]).
to((source) => any value)- Transform the field value before returning it.
- It accepts any value as return type.
withDefault(value):- Return the default value if the field is null or undefined.
- It accepts any value as return type.
undefinedAsNull():- Return null if the field is undefined.
You can freely chain and compose these methods to transform and pass data however you like. If the way you compose them results in a type mismatch with your schema (such as returning the wrong value), you'll get a type error right when defining the type fields in User.$fields({...}).
The Query resolvers are implemented as any other type resolvers.
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
Resolvers receive the following parameters:
source: The parent object.- See custom types on how to customize the type of the source.
args: The arguments of the field.ctx: The context object.- See custom types on how to customize the type of the context.
info: The GraphQL info object.- See custom types on how to customize the type of the info.
Extending Types in a Separate Module
src/modules/user-extended/user-posts.gql:
extend type User {
fullName: String!
posts: [Post!]
}
Implementing Extended Resolvers
After defining your schema, implement the resolvers in their respective modules 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,
});
Best Practices
- Module Organization
- Keep related schema and resolvers together
- Use meaningful file names
- Follow consistent module structure
- Type Safety
- Leverage generated types from schema
- Enable strict type checking
- Error Handling
- User appropriate GraphQL errors
- Provide meaningful error messages