Resolvers
Baeta provides a modular, type-safe approach to defining GraphQL resolvers. Let's look at how to implement user resolvers.
Project Structure
src/modules/
└── user/
├── user.gql
├── resolvers.ts
└── typedef.ts
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
}
input UserWhereUniqueInput {
id: ID!
}
type Query {
user(where: UserWhereUniqueInput!): User
users: [User!]
}
Implement Resolvers
After defining your schema, implement the resolvers in src/modules/user/resolvers.ts
:
import { getUserModule } from "./typedef.ts";
import { UserDataSource } from "./datasource.ts";
const { Query } = getUserModule();
// Single user resolver
Query.user(({ args, ctx }) => {
return ctx.dataSources.users.findUnique(args.where);
});
// Users list resolver
Query.users(({ ctx }) => {
return ctx.dataSources.users.findMany();
});
Resolver Context
Resolvers receive the context object automatically that you define in the baeta.ts
config file as you can see Context, which contains the data sources:
interface Context {
dataSources: OrmDataSource;
}
Type Safety
Baeta provides full type safety for:
- Arguments
- Return types
- Parent types
- Context
// TypeScript ensures type safety
Query.user(({ args, ctx }) => {
// args.where is typed as UserWhereUniqueInput
// return type must match User schema
return ctx.dataSources.users.findUnique(args.where);
});
Field Resolvers
Baeta allows you to define resolvers for any field in your types, not just Query fields:
const { User } = getUserModule();
User.email(({ root }) => {
return root.email.toLowerCase();
});
Caveats
When you define a required field in a type (like fullName: String!
), that field must be resolved in all resolvers that return that type. In our example, because fullName
is marked as non-nullable (String!
), both Query.user
and Query.users
would need to include this field in their return data.
To add computed required fields without modifying existing resolvers, create a separate module that extends the base type instead. If the field was optional (String
instead of String!
), this wouldn't be necessary.
Extending Types in a Separate Module
src/modules/user-extended/user-extended.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-extended/resolvers.ts
:
import { getUserExtendedModule } from "./typedef.ts";
const { User } = getUserExtendedModule();
User.fullName(({ root }) => {
return `${root.givenName} ${root.lastName}`;
});
User.posts(({ root, ctx }) => {
return ctx.dataSources.posts.findMany({
where: { authorId: root.id },
});
});
Best Practices
- Module Organization
- Keep related schema and resolvers together
- Use meaningful file names
- Follow consistent module structure
- Type Safety
- Leverage TypeScript's type system
- Use generated types from schema
- Enable strict type checking
- Error Handling
- User appropriate GraphQL errors
- Provide meaningful error messages
- Handle edge cases