GraphQL with NestJS
GraphQL is a query language for APIs that lets clients ask for exactly the data they need in a single round trip, against a strongly typed schema. NestJS provides first-class GraphQL support through the @nestjs/graphql package, which layers Nest’s familiar decorators, dependency injection, and module system on top of a pluggable server driver. This means you write resolvers the same way you write controllers — as injectable classes — while the driver handles schema serving, validation, and the HTTP transport.
Installing the GraphQL module
The @nestjs/graphql package is driver-agnostic: it defines the integration contract but does not bundle a server. You choose a driver — Apollo or Mercurius — and install its companion package alongside graphql itself.
# Apollo (Express-based) driver
npm install @nestjs/graphql @nestjs/apollo @apollo/server graphql
# Mercurius (Fastify-based) driver
npm install @nestjs/graphql @nestjs/mercurius graphql
You then register GraphQLModule.forRoot() in your root module, passing the driver class and its options. The example below uses the Apollo driver in code-first mode, where the schema is generated from your TypeScript classes and written to a file.
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
import { RecipesModule } from './recipes/recipes.module';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
playground: true,
}),
RecipesModule,
],
})
export class AppModule {}
The driver option is the only required field; everything else is forwarded to the underlying server. Because forRoot is generic over the driver config type, TypeScript will type-check options against the driver you actually chose.
Apollo vs Mercurius
Both drivers expose the same @nestjs/graphql decorators, so resolver code is portable between them. The difference lies in the HTTP platform they sit on and their performance characteristics.
| Aspect | Apollo (@nestjs/apollo) | Mercurius (@nestjs/mercurius) |
|---|---|---|
| HTTP platform | Express (default) or Fastify | Fastify only |
| Underlying server | Apollo Server 4 | Mercurius |
| Ecosystem | Largest plugin/tooling ecosystem | Lean, Fastify-native |
| Throughput | Excellent | Higher raw throughput |
| Federation | Apollo Federation v1/v2 | Federation supported |
| Best when | You want the widest tooling support | You run on Fastify and want max performance |
Mercurius requires the Fastify HTTP adapter. If your app was bootstrapped with the default Express adapter, switch to
NestFactory.create(AppModule, new FastifyAdapter())before registering the Mercurius driver, or registration will fail.
In practice, choose Apollo if you value the mature ecosystem (Apollo Studio, the broadest set of community plugins, well-trodden federation tooling). Choose Mercurius if you are already committed to Fastify and want its lower per-request overhead.
The GraphQL request lifecycle
A GraphQL request differs structurally from a REST request. REST exposes many endpoints, each returning a fixed shape; GraphQL exposes a single endpoint (/graphql) and resolves an arbitrary query tree at runtime. A query first hits the operation type — Query, Mutation, or Subscription — and the driver then walks the requested fields, invoking a resolver function for each one.
import { Resolver, Query, Args } from '@nestjs/graphql';
import { Recipe } from './recipe.model';
import { RecipesService } from './recipes.service';
@Resolver(() => Recipe)
export class RecipesResolver {
constructor(private readonly recipesService: RecipesService) {}
@Query(() => [Recipe], { name: 'recipes' })
findAll(): Promise<Recipe[]> {
return this.recipesService.findAll();
}
@Query(() => Recipe, { name: 'recipe', nullable: true })
findOne(@Args('id') id: string): Promise<Recipe | null> {
return this.recipesService.findOne(id);
}
}
A client now selects only the fields it wants:
query {
recipes {
id
title
}
}
Output:
{
"data": {
"recipes": [
{ "id": "1", "title": "Lemon Risotto" },
{ "id": "2", "title": "Miso Ramen" }
]
}
}
Nest’s request pipeline — guards, interceptors, and pipes — still applies, but it runs per resolver rather than per route. Field-level resolution also means a single query can trigger many resolver calls, which is why batching patterns (such as DataLoader) matter more than in REST.
Where GraphQL fits versus REST
GraphQL shines when clients have diverse data needs, when you want to avoid over- and under-fetching, or when you are aggregating several backend sources behind one typed graph. REST remains a strong fit for simple CRUD surfaces, file uploads/downloads, heavy HTTP caching, and public APIs where resource-oriented URLs are an asset. The two are not mutually exclusive — a NestJS app can serve REST controllers and a GraphQL endpoint side by side from the same modules and services.
Best Practices
- Pick one driver per application and pin its companion package version to the
@nestjs/graphqlmajor version to avoid schema/runtime mismatches. - Prefer code-first with
autoSchemaFileso your schema is generated from typed classes and stays in sync with your resolvers. - Disable
playgroundand introspection in production, or gate them behind authentication. - Keep resolvers thin — delegate business logic to injectable services, exactly as you do with controllers.
- Use DataLoader for nested field resolvers to prevent N+1 query explosions across the resolution tree.
- Reuse Nest guards, interceptors, and pipes in GraphQL via
GqlExecutionContextrather than writing GraphQL-specific duplicates.