GraphQL with NestJS
Integrating GraphQL into a NestJS application using Apollo Server or other GraphQL libraries.
Defining GraphQL Schema in NestJS
Introduction
NestJS provides excellent support for building GraphQL APIs. A crucial aspect of any GraphQL API is its schema, which defines the shape of the data that clients can query. NestJS offers two primary approaches for defining GraphQL schemas: schema-first and code-first. This document explores both approaches and demonstrates how to utilize NestJS's decorators to create and manage your GraphQL schema effectively.
Schema-First Approach
The schema-first approach involves defining the schema using GraphQL's Schema Definition Language (SDL) and then mapping resolvers to the schema elements. This allows you to clearly define the API contract upfront.
Steps for Schema-First
- Define the Schema (
.graphql
file): Create a.graphql
file to define your types, queries, and mutations. - Create Resolvers: Implement resolvers that fetch or modify data based on the queries and mutations defined in your schema.
- Configure
GraphQLModule
: In your NestJS module (e.g.,AppModule
), configure theGraphQLModule
to point to your schema file.
Example
src/schema.graphql
type Query {
hello: String
getTodo(id: Int!): Todo
todos: [Todo!]!
}
type Mutation {
createTodo(title: String!, description: String): Todo!
updateTodo(id: Int!, title: String, description: String): Todo!
deleteTodo(id: Int!): Boolean!
}
type Todo {
id: Int!
title: String!
description: String
completed: Boolean!
}
src/app.resolver.ts
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { TodoService } from './todo.service';
import { Todo } from './todo.model';
import { CreateTodoInput, UpdateTodoInput } from './todo.input';
@Resolver(() => Todo)
export class AppResolver {
constructor(private readonly todoService: TodoService) {}
@Query(() => String)
hello(): string {
return 'Hello world!';
}
@Query(() => Todo, { nullable: true })
getTodo(@Args('id', { type: () => Int }) id: number): Promise<Todo> {
return this.todoService.getTodo(id);
}
@Query(() => [Todo])
todos(): Promise<Todo[]> {
return this.todoService.getAllTodos();
}
@Mutation(() => Todo)
createTodo(@Args('createTodoInput') createTodoInput: CreateTodoInput): Promise<Todo> {
return this.todoService.createTodo(createTodoInput);
}
@Mutation(() => Todo)
updateTodo(@Args('id', { type: () => Int }) id: number, @Args('updateTodoInput') updateTodoInput: UpdateTodoInput): Promise<Todo> {
return this.todoService.updateTodo(id, updateTodoInput);
}
@Mutation(() => Boolean)
deleteTodo(@Args('id', { type: () => Int }) id: number): Promise<boolean> {
return this.todoService.deleteTodo(id);
}
}
src/app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { AppResolver } from './app.resolver';
import { TodoService } from './todo.service';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: 'src/schema.graphql', // or join(process.cwd(), 'src/schema.graphql'),
sortSchema: true, // optional
debug: true, // optional
playground: true, // optional
}),
],
providers: [AppResolver, TodoService],
})
export class AppModule {}
Code-First Approach
The code-first approach uses TypeScript decorators to define the schema directly within your code. NestJS handles the schema generation based on these decorators. This tightly couples the schema to your code, which can streamline development when you're comfortable with TypeScript.
Steps for Code-First
- Define Types and Resolvers with Decorators: Use decorators like
@ObjectType
,@Field
,@Query
, and@Mutation
to define your GraphQL types and resolvers directly in your TypeScript classes. - Configure
GraphQLModule
: Configure theGraphQLModule
to generate the schema automatically based on the decorated classes.
Example
src/todo.model.ts
import { ObjectType, Field, Int } from '@nestjs/graphql';
@ObjectType()
export class Todo {
@Field(() => Int)
id: number;
@Field()
title: string;
@Field({ nullable: true })
description?: string;
@Field()
completed: boolean;
}
src/todo.input.ts
import { InputType, Field } from '@nestjs/graphql';
@InputType()
export class CreateTodoInput {
@Field()
title: string;
@Field({ nullable: true })
description?: string;
}
@InputType()
export class UpdateTodoInput {
@Field({ nullable: true })
title?: string;
@Field({ nullable: true })
description?: string;
}
src/app.resolver.ts
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { TodoService } from './todo.service';
import { Todo } from './todo.model';
import { CreateTodoInput, UpdateTodoInput } from './todo.input';
@Resolver(() => Todo)
export class AppResolver {
constructor(private readonly todoService: TodoService) {}
@Query(() => String)
hello(): string {
return 'Hello world!';
}
@Query(() => Todo, { nullable: true })
getTodo(@Args('id', { type: () => Int }) id: number): Promise<Todo> {
return this.todoService.getTodo(id);
}
@Query(() => [Todo])
todos(): Promise<Todo[]> {
return this.todoService.getAllTodos();
}
@Mutation(() => Todo)
createTodo(@Args('createTodoInput') createTodoInput: CreateTodoInput): Promise<Todo> {
return this.todoService.createTodo(createTodoInput);
}
@Mutation(() => Todo)
updateTodo(@Args('id', { type: () => Int }) id: number, @Args('updateTodoInput') updateTodoInput: UpdateTodoInput): Promise<Todo> {
return this.todoService.updateTodo(id, updateTodoInput);
}
@Mutation(() => Boolean)
deleteTodo(@Args('id', { type: () => Int }) id: number): Promise<boolean> {
return this.todoService.deleteTodo(id);
}
}
src/app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { AppResolver } from './app.resolver';
import { TodoService } from './todo.service';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true, // Generates schema.gql in memory or a file (default)
sortSchema: true, // optional
debug: true, // optional
playground: true, // optional
}),
],
providers: [AppResolver, TodoService],
})
export class AppModule {}
Utilizing NestJS Decorators
NestJS provides several decorators to simplify GraphQL schema definition in the code-first approach. Here's a breakdown of some key decorators:
@ObjectType()
: Marks a class as a GraphQL object type.@Field(() => Type, options)
: Defines a field within an object type. TheType
argument specifies the field's data type (e.g.,String
,Int
, or a custom object type). Theoptions
argument allows specifying properties likenullable
,description
, etc.@InputType()
: Marks a class as a GraphQL input type. Input types are used for arguments in mutations.@Resolver(of?: Function | string)
: Marks a class as a GraphQL resolver. The optionalof
argument specifies the object type that this resolver handles.@Query(returns?: (type?: ReturnTypeFunction) => any, options?: ResolverOptions)
: Marks a method as a GraphQL query. Thereturns
function defines the return type of the query.@Mutation(returns?: (type?: ReturnTypeFunction) => any, options?: ResolverOptions)
: Marks a method as a GraphQL mutation. Thereturns
function defines the return type of the mutation.@Args(name: string, options?: ParamDecoratorOptions)
: Injects arguments into a resolver method. Used to access the data sent by the client in a query or mutation.@Args('id', { type: () => Int }) id: number
is common.@Int()
,@String()
,@Boolean()
,@Float()
,@ID()
: These are Type decorators for Fields.
Choosing the Right Approach
The choice between schema-first and code-first depends on your project's requirements and development style.
- Schema-First:
- Pros: Clear separation of concerns, good for API-first development, easier collaboration with frontend developers.
- Cons: Requires maintaining two sets of definitions (schema and resolvers), potential for mismatch between schema and resolvers.
- Code-First:
- Pros: Faster development, type safety through TypeScript, less boilerplate code.
- Cons: Tighter coupling between schema and code, potentially harder to understand the schema without looking at the code, can be harder to collaborate with frontend teams relying on SDL first.
In general, if you prioritize a clear API contract and collaboration, the schema-first approach is a good choice. If you prioritize rapid development and type safety, the code-first approach might be more suitable.