Skip to content
NestJS ns graphql 4 min read

Resolvers

Resolvers are the heart of a NestJS GraphQL API: they are the functions that actually produce the data for each field in your schema. In the code-first approach, you write plain TypeScript classes decorated with @Resolver, and methods decorated with @Query or @Mutation become the entry points clients can call. Because resolvers are ordinary @Injectable-style providers, they participate fully in Nest’s dependency injection, so you can wire in services, repositories, and other collaborators exactly as you would in a REST controller.

Anatomy of a resolver

A resolver is a class annotated with @Resolver(). Inside it, each query or mutation maps to a method whose return type drives the generated schema. The @Query and @Mutation decorators take a function returning the GraphQL type (() => User, () => [User], etc.), which Nest uses to build the SDL at startup.

import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { NotFoundException } from '@nestjs/common';
import { User } from './models/user.model';
import { CreateUserInput } from './dto/create-user.input';
import { UsersService } from './users.service';

@Resolver(() => User)
export class UsersResolver {
  constructor(private readonly usersService: UsersService) {}

  @Query(() => [User], { name: 'users' })
  findAll(): User[] {
    return this.usersService.findAll();
  }

  @Query(() => User, { name: 'user', nullable: true })
  findOne(@Args('id', { type: () => Int }) id: number): User {
    const user = this.usersService.findOne(id);
    if (!user) throw new NotFoundException(`User ${id} not found`);
    return user;
  }

  @Mutation(() => User)
  createUser(@Args('input') input: CreateUserInput): User {
    return this.usersService.create(input);
  }
}

Register the resolver in a module just like any provider. Resolvers are not declared in controllers; they live alongside services in providers.

import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service';

@Module({
  providers: [UsersResolver, UsersService],
})
export class UsersModule {}

Reading arguments with @Args

The @Args decorator binds named GraphQL arguments to method parameters. For scalar types Nest can infer the type, but for number you must be explicit because TypeScript erases number to either Int or Float — Nest cannot tell which. Pass { type: () => Int } (or Float) to disambiguate.

@Query(() => [User])
search(
  @Args('term') term: string,
  @Args('limit', { type: () => Int, nullable: true, defaultValue: 10 })
  limit: number,
): User[] {
  return this.usersService.search(term, limit);
}

For richer inputs, group fields into an @InputType() class and accept the whole object. This keeps signatures clean and lets you attach validation via class-validator and a global ValidationPipe.

OptionPurpose
type: () => IntDisambiguate numeric scalars (Int vs Float)
nullable: trueAllow the argument to be omitted or null
defaultValueProvide a default when the client omits the argument
name: 'foo'Override the schema field/argument name

The argument name passed to @Args('id') is what clients use in queries — it does not have to match the TypeScript parameter name, but keeping them aligned avoids confusion.

Returning object types

A resolver method must return data shaped like its declared @ObjectType. You can return the entity directly when its fields match, or map a persistence model into a GraphQL model. The example below shows a query executed against the resolver above.

curl -s http://localhost:3000/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query":"{ user(id: 1) { id name email } }"}'

Output:

{"data":{"user":{"id":1,"name":"Ada Lovelace","email":"[email protected]"}}}

Structuring resolvers per object type

A healthy convention is one resolver class per object type, declared with @Resolver(() => User). The type argument is required when you add field resolvers (see the related page), and it documents which entity the class owns. Cross-cutting or top-level queries that do not belong to a single type can live in a dedicated resolver such as AppResolver.

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

  @Query(() => [Post])
  posts(@Args('authorId', { type: () => Int, nullable: true }) authorId?: number): Post[] {
    return authorId ? this.postsService.byAuthor(authorId) : this.postsService.findAll();
  }

  @Mutation(() => Boolean)
  deletePost(@Args('id', { type: () => Int }) id: number): boolean {
    return this.postsService.remove(id);
  }
}

Async work is fully supported: return a Promise or an Observable and Nest awaits it before serializing the response. This is the normal case once you talk to a database or external API.

@Query(() => User, { nullable: true })
async profile(@Args('id', { type: () => Int }) id: number): Promise<User | null> {
  return this.usersService.findById(id);
}

Best practices

  • Keep one resolver per object type and delegate all real work to @Injectable services — resolvers should orchestrate, not implement business logic.
  • Always specify { type: () => Int } (or Float) for numeric arguments and return types; relying on inference for number will break the schema.
  • Prefer @InputType objects over long lists of @Args so inputs are reusable and validatable.
  • Throw Nest exceptions (or domain errors) from resolvers and let an exception filter shape the GraphQL error response rather than returning null silently.
  • Mark fields and arguments nullable deliberately; an accidentally non-null field that resolves to null produces a hard query error.
  • Register resolvers under providers, never controllers, and import the owning feature module where the resolver is needed.
Last updated June 14, 2026
Was this helpful?