Resolvers

Typically, you have to create a resolvers map manually. The @nestjs/graphql package, on the other hand, generate resolvers map automatically using the metadata provided by the decorators. In order to learn the library basics, we’ll create a simple authors API.

Schema first

As mentioned in the previous chapter, in the schema first approach we have to manually define our types in SDL (read more).

  1. type Author {
  2. id: Int!
  3. firstName: String
  4. lastName: String
  5. posts: [Post]
  6. }
  7. type Post {
  8. id: Int!
  9. title: String!
  10. votes: Int
  11. }
  12. type Query {
  13. author(id: Int!): Author
  14. }

Our GraphQL schema contains single query exposed - author(id: Int!): Author. Now, let’s create an AuthorResolver.

  1. @Resolver('Author')
  2. export class AuthorResolver {
  3. constructor(
  4. private readonly authorsService: AuthorsService,
  5. private readonly postsService: PostsService,
  6. ) {}
  7. @Query()
  8. async author(@Args('id') id: number) {
  9. return await this.authorsService.findOneById(id);
  10. }
  11. @ResolveProperty()
  12. async posts(@Parent() author) {
  13. const { id } = author;
  14. return await this.postsService.findAll({ authorId: id });
  15. }
  16. }

info Hint If you use the @Resolver() decorator, you don’t have to mark a class as an @Injectable(), otherwise, it’s necessary.

The @Resolver() decorator does not affect queries and mutations (neither @Query() nor @Mutation() decorators). It only informs Nest that each @ResolveProperty() inside this particular class has a parent, which is an Author type in this case (Author.posts relation). Basically, instead of setting @Resolver() at the top of the class, this can be done close to the method:

  1. @Resolver('Author')
  2. @ResolveProperty()
  3. async posts(@Parent() author) {
  4. const { id } = author;
  5. return await this.postsService.findAll({ authorId: id });
  6. }

However, if you have multiple @ResolveProperty() inside one class, you would have to add @Resolver() to all of them which is not necessarily a good practice (creates an extra overhead).

Conventionally, we would use something like getAuthor() or getPosts() as method names. We can easily do this by moving the real names between the parentheses of the decorator.

  1. @Resolver('Author')
  2. export class AuthorResolver {
  3. constructor(
  4. private readonly authorsService: AuthorsService,
  5. private readonly postsService: PostsService,
  6. ) {}
  7. @Query('author')
  8. async getAuthor(@Args('id') id: number) {
  9. return await this.authorsService.findOneById(id);
  10. }
  11. @ResolveProperty('posts')
  12. async getPosts(@Parent() author) {
  13. const { id } = author;
  14. return await this.postsService.findAll({ authorId: id });
  15. }
  16. }

info Hint The @Resolver() decorator can be used at the method-level as well.

Typings

Assuming that we have enabled the typings generation feature (with outputAs: 'class') in the previous chapter, once you run our application it should generate the following file:

  1. export class Author {
  2. id: number;
  3. firstName?: string;
  4. lastName?: string;
  5. posts?: Post[];
  6. }
  7. export class Post {
  8. id: number;
  9. title: string;
  10. votes?: number;
  11. }
  12. export abstract class IQuery {
  13. abstract author(id: number): Author | Promise<Author>;
  14. }

Classes allow you using decorators which makes them extremely useful in terms of the validation purposes (read more). For example:

  1. import { MinLength, MaxLength } from 'class-validator';
  2. export class CreatePostInput {
  3. @MinLength(3)
  4. @MaxLength(50)
  5. title: string;
  6. }

warning Notice To enable auto-validation of your inputs (and parameters), you have to use ValidationPipe. Read more about validation here or more specifically about pipes here.

Nonetheless, if you add your decorators directly into the automatically generated file, they will be thrown away on each consecutive change. Hence, you should rather create a separate file and simply extend the generated class.

  1. import { MinLength, MaxLength } from 'class-validator';
  2. import { Post } from '../../graphql.ts';
  3. export class CreatePostInput extends Post {
  4. @MinLength(3)
  5. @MaxLength(50)
  6. title: string;
  7. }

Code first

In the code first approach, we don’t have to write SDL by hand. Instead we’ll only use decorators.

  1. import { Field, Int, ObjectType } from 'type-graphql';
  2. import { Post } from './post';
  3. @ObjectType()
  4. export class Author {
  5. @Field(type => Int)
  6. id: number;
  7. @Field({ nullable: true })
  8. firstName?: string;
  9. @Field({ nullable: true })
  10. lastName?: string;
  11. @Field(type => [Post])
  12. posts: Post[];
  13. }

Author model has been created. Now, let’s create the missing Post class.

  1. import { Field, Int, ObjectType } from 'type-graphql';
  2. @ObjectType()
  3. export class Post {
  4. @Field(type => Int)
  5. id: number;
  6. @Field()
  7. title: string;
  8. @Field(type => Int, { nullable: true })
  9. votes?: number;
  10. }

Since our models are ready, we can move to the resolver class.

  1. @Resolver(of => Author)
  2. export class AuthorResolver {
  3. constructor(
  4. private readonly authorsService: AuthorsService,
  5. private readonly postsService: PostsService,
  6. ) {}
  7. @Query(returns => Author)
  8. async author(@Args({ name: 'id', type: () => Int }) id: number) {
  9. return await this.authorsService.findOneById(id);
  10. }
  11. @ResolveProperty()
  12. async posts(@Parent() author) {
  13. const { id } = author;
  14. return await this.postsService.findAll({ authorId: id });
  15. }
  16. }

Conventionally, we would use something like getAuthor() or getPosts() as method names. We can easily do this by moving the real names to the decorators.

  1. @Resolver(of => Author)
  2. export class AuthorResolver {
  3. constructor(
  4. private readonly authorsService: AuthorsService,
  5. private readonly postsService: PostsService,
  6. ) {}
  7. @Query(returns => Author, { name: 'author' })
  8. async getAuthor(@Args({ name: 'id', type: () => Int }) id: number) {
  9. return await this.authorsService.findOneById(id);
  10. }
  11. @ResolveProperty('posts')
  12. async getPosts(@Parent() author) {
  13. const { id } = author;
  14. return await this.postsService.findAll({ authorId: id });
  15. }
  16. }

Usually, you won’t have to pass such an object into the @Args() decorator. For example, if your identifier’s type would be a string, the following construction would be sufficient:

  1. @Args('id') id: string

However, the number type doesn’t give type-graphql enough information about the expected GraphQL representation (Int vs Float) and thus, we have to explicitly pass the type reference.

Moreover, you can create a dedicated AuthorArgs class:

  1. @Args() id: AuthorArgs

With the following body:

  1. @ArgsType()
  2. class AuthorArgs {
  3. @Field(type => Int)
  4. @Min(1)
  5. id: number;
  6. }

info Hint Both @Field() and @ArgsType() decorators are imported from the type-graphql package, while @Min() comes from the class-validator.

You may also notice that such classes play very well with the ValidationPipe (read more).

Decorators

You may note that we refer to the following arguments using dedicated decorators. Below is a comparison of the provided decorators and the plain Apollo parameters they represent.

@Root() and @Parent() root/parent
@Context(param?: string) context / context[param]
@Info(param?: string) info / info[param]
@Args(param?: string) args / args[param]

Module

Once we’re done here, we have to register the AuthorResolver somewhere, for example inside the newly created AuthorsModule.

  1. @Module({
  2. imports: [PostsModule],
  3. providers: [AuthorsService, AuthorResolver],
  4. })
  5. export class AuthorsModule {}

The GraphQLModule will take care of reflecting the metadata and transforming class into the correct resolvers map automatically. The only thing that you should be aware of is that you need to import this module somewhere, therefore Nest will know that AuthorsModule truly exists.

info Hint Learn more about GraphQL queries here.