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

  1. Define the Schema (.graphql file): Create a .graphql file to define your types, queries, and mutations.
  2. Create Resolvers: Implement resolvers that fetch or modify data based on the queries and mutations defined in your schema.
  3. Configure GraphQLModule: In your NestJS module (e.g., AppModule), configure the GraphQLModule 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

  1. 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.
  2. Configure GraphQLModule: Configure the GraphQLModule 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. The Type argument specifies the field's data type (e.g., String, Int, or a custom object type). The options argument allows specifying properties like nullable, 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 optional of argument specifies the object type that this resolver handles.
  • @Query(returns?: (type?: ReturnTypeFunction) => any, options?: ResolverOptions): Marks a method as a GraphQL query. The returns function defines the return type of the query.
  • @Mutation(returns?: (type?: ReturnTypeFunction) => any, options?: ResolverOptions): Marks a method as a GraphQL mutation. The returns 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.