Resolvers and Arguments

A resolver is a provider whose decorated methods back schema fields, and @Args turns typed parameters into GraphQL arguments.

Syntax@Query(() => Author, { name: 'author' }) method(@Args('id', { type: () => Int }) id: number)

A resolver is an ordinary provider — a class Nest constructs and injects dependencies into — whose methods are wired to fields in the schema. You register it in a module's providers array exactly like a service.

@Query(() => Author) adds a field to the root Query type; @Mutation() adds one to Mutation. The function argument exists because TypeScript types are gone at runtime, so the schema builder needs to be told what to emit. The field name is the method name unless you pass { name: 'author' }.

Arguments

@Args('id') pulls one argument out of the incoming query and binds it to a parameter. A number needs { type: () => Int } or you get Float in the schema. Past two or three arguments, declare an @ArgsType() class and take @Args() with no name: same arguments in the schema, one object in the method. An @InputType() class is a different thing — it becomes a named input type, so the client sends input: { ... } as one argument.

Field resolvers

@ResolveField() backs a field on a non-root type. @Resolver(() => Author) is what tells Nest which type those methods belong to. The method receives the already-loaded parent object through @Parent(), and runs only if the client selected that field — after the parent resolved, once per parent object.

Ordering

Per request: HTTP middleware once on /graphql, then, for each resolver, guards, interceptors, pipes (so @Args validation), the method, interceptors again, filters. In @nestjs/graphql 14 fieldResolverEnhancers defaults to empty, and the only values it accepts are 'guards', 'interceptors' and 'filters', so none of those three run on @ResolveField() until you opt in. Pipes are not an enhancer: they always run, so @Args validation works on a field resolver with no configuration.

Example

Example · typescript
// src/authors/authors.resolver.ts
import {
  Args,
  Int,
  Parent,
  Query,
  ResolveField,
  Resolver,
} from '@nestjs/graphql';
import { Author } from './author.model.js';
import { Post } from '../posts/post.model.js';
import { AuthorsService } from './authors.service.js';
import { PostsService } from '../posts/posts.service.js';

// The type argument is what makes @ResolveField() below legal: it names the
// object type whose fields this class resolves.
@Resolver(() => Author)
export class AuthorsResolver {
  // Plain constructor injection. A resolver is a provider like any other.
  constructor(
    private readonly authors: AuthorsService,
    private readonly posts: PostsService,
  ) {}

  // Schema: author(id: Int!): Author
  // The field is called `author` because of `name`, not because of the method
  // name; nullable: true is what drops the trailing `!` on the return type.
  @Query(() => Author, { name: 'author', nullable: true })
  async getAuthor(@Args('id', { type: () => Int }) id: number) {
    return this.authors.findOneById(id);
  }

  // Schema: Author.posts: [Post!]!
  // Runs only when the client selects `posts`, after the parent author has
  // been resolved, once per author in the result.
  @ResolveField('posts', () => [Post])
  async getPosts(@Parent() author: Author) {
    return this.posts.findByAuthor(author.id);
  }
}

When to use it

  • Expose an existing service method as a query without changing the service.
  • Let one request fetch an author and that author's posts instead of two round trips.
  • Group skip, take and orderBy into one @ArgsType class so pagination is declared and validated once.
  • Accept a nested object from a mutation as a named input type the client can bind to a variable.

More examples

Inline arguments and @ArgsType() are the same schema

Both queries publish identical arguments; the only difference is where they are declared. Reach for @ArgsType() once pagination or filter arguments repeat across queries, or once you want class-validator rules on them — and recognise the short form when you meet @Args() with no string in someone else's resolver.

Example · typescript
// src/authors/dto/authors.args.ts
import { ArgsType, Field, Int } from '@nestjs/graphql';
import { Max, Min } from 'class-validator';

@ArgsType()
export class AuthorsArgs {
  @Field(() => Int, { defaultValue: 0 })
  @Min(0)
  skip: number;

  @Field(() => Int, { defaultValue: 25 })
  @Min(1)
  @Max(100)
  take: number;
}

// src/authors/authors-list.resolver.ts
import { Args, Int, Query, Resolver } from '@nestjs/graphql';
import { Author } from './author.model.js';
import { AuthorsArgs } from './dto/authors.args.js';
import { AuthorsService } from './authors.service.js';

@Resolver(() => Author)
export class AuthorsListResolver {
  constructor(private readonly authors: AuthorsService) {}

  // Longhand: one @Args() per argument.
  // Schema: authorsLonghand(skip: Int! = 0, take: Int! = 25): [Author!]!
  @Query(() => [Author], { name: 'authorsLonghand' })
  authorsLonghand(
    @Args('skip', { type: () => Int, defaultValue: 0 }) skip: number,
    @Args('take', { type: () => Int, defaultValue: 25 }) take: number,
  ) {
    return this.authors.findAll({ skip, take });
  }

  // Shorthand: @Args() with no name spreads the @ArgsType() class.
  // Schema: authors(skip: Int! = 0, take: Int! = 25): [Author!]!
  // Same two arguments on the wire, and the class-validator rules above
  // now apply because ValidationPipe can see the metatype.
  @Query(() => [Author], { name: 'authors' })
  authorsShorthand(@Args() args: AuthorsArgs) {
    return this.authors.findAll(args);
  }
}

A mutation with an @InputType() and the request context

Use @InputType() when the client should be able to name the shape — it can then be declared as a GraphQL variable and reused. @ArgsType() cannot: it is flattened into loose arguments. The @Context('req') parameter shows how a resolver reaches per-request state such as the authenticated user, which has no equivalent of @Req() in GraphQL.

Example · typescript
// src/posts/dto/create-post.input.ts
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, Length } from 'class-validator';

// @InputType() becomes a real named type in the schema:
//   input CreatePostInput { title: String!, body: String! }
@InputType()
export class CreatePostInput {
  @Field()
  @Length(3, 120)
  title: string;

  @Field()
  @IsNotEmpty()
  body: string;
}

// src/posts/posts.resolver.ts
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
import { Post } from './post.model.js';
import { CreatePostInput } from './dto/create-post.input.js';
import { PostsService } from './posts.service.js';

@Resolver(() => Post)
export class PostsResolver {
  constructor(private readonly posts: PostsService) {}

  // Schema: createPost(input: CreatePostInput!): Post!
  @Mutation(() => Post)
  createPost(
    @Args('input') input: CreatePostInput,
    // @Context('loaders') style access: the string picks one property off the
    // context object the module's `context` factory returned.
    @Context('req') req: { user: { id: string } },
  ) {
    return this.posts.create(input, req.user.id);
  }
}

Turning enhancers on for field resolvers

This is the fix for the most common silent failure in a Nest GraphQL app: a guard on a field resolver that never runs, so a protected relation is readable by anyone who nests into it. Set it the moment you put @UseGuards() on anything decorated with @ResolveField().

Example · typescript
// src/app.module.ts (GraphQL section only)
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: true,
      // Default is [] in @nestjs/graphql 14: a @UseGuards() on a
      // @ResolveField() method is ignored until the enhancer is listed here.
      // Each entry costs one pass per field per parent object, so enable only
      // what you need.
      fieldResolverEnhancers: ['guards', 'interceptors', 'filters'],
    }),
  ],
})
export class AppModule {}

Discussion

  • Be the first to comment on this lesson.