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.
| Option | Purpose |
|---|---|
type: () => Int | Disambiguate numeric scalars (Int vs Float) |
nullable: true | Allow the argument to be omitted or null |
defaultValue | Provide 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
@Injectableservices — resolvers should orchestrate, not implement business logic. - Always specify
{ type: () => Int }(orFloat) for numeric arguments and return types; relying on inference fornumberwill break the schema. - Prefer
@InputTypeobjects over long lists of@Argsso 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
nullsilently. - Mark fields and arguments
nullabledeliberately; an accidentally non-null field that resolves tonullproduces a hard query error. - Register resolvers under
providers, nevercontrollers, and import the owning feature module where the resolver is needed.