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

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:

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 .map but enforces the return type to be of the expected value.
    • Can be async.
  • 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.
tip

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.

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

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

  1. Module Organization
  • Keep related schema and resolvers together
  • Use meaningful file names
  • Follow consistent module structure
  1. Type Safety
  • Leverage generated types from schema
  • Enable strict type checking
  1. Error Handling