Nest JS with Graphql Building APIs

Nest JS with Graphql Building APIs

Harnessing the power of TypeScript & GraphQL

GraphQL is a powerful query language for APIs and a runtime for fulfilling those queries with your existing data. It's an elegant approach that solves many problems typically found with REST APIs. For background, we suggest reading this comparison between GraphQL and REST. GraphQL combined with TypeScript helps you develop better type safety with your GraphQL queries, giving you end-to-end typing.

In this chapter, we assume a basic understanding of GraphQL, and focus on how to work with the built-in @nestjs/graphql module. The GraphQLModule is a wrapper around the Apollo server. We use this proven GraphQL package to provide a way to use GraphQL with Nest.

Installation

Start by installing the required packages:

$ npm i @nestjs/graphql graphql-tools graphql apollo-server-express

Nest offers two ways of building GraphQL applications, the code first and the schema first methods. You should choose the one that works best for you. Most of the chapters in this GraphQL section are divided into two main parts: one you should follow if you adopt code first, and the other to be used if you adopt schema first.

In the code first approach, you use decorators and TypeScript classes to generate the corresponding GraphQL schema. This approach is useful if you prefer to work exclusively with TypeScript and avoid context switching between language syntaxes.

In the schema first approach, the source of truth is GraphQL SDL (Schema Definition Language) files. SDL is a language-agnostic way to share schema files between different platforms. Nest automatically generates your TypeScript definitions (using either classes or interfaces) based on the GraphQL schemas to reduce the need to write redundant boilerplate code.

Getting started with GraphQL & TypeScript# Once the packages are installed, we can import the GraphQLModule and configure it with the forRoot() static method.


import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot({}),
  ],
})
export class AppModule {}

The forRoot() method takes an options object as an argument. These options are passed through to the underlying Apollo instance (read more about available settings here). For example, if you want to disable the playground and turn off debug mode, pass the following options:

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot({
      debug: false,
      playground: false,
    }),
  ],
})
export class AppModule {}

As mentioned, these options will be forwarded to the ApolloServer constructor.

Lets Build Application from scratch

we will get GraphQLModule from '@nestjs/graphql' and will initilize graphql in graphql service

import { Module, CacheModule } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { GraphQLModule } from '@nestjs/graphql'
import { GraphqlModule } from './config/graphql/graphql.module'
import { GraphqlService } from './config/graphql/graphql.service'
@Module({
	imports: [
		GraphQLModule.forRootAsync({
			useClass: GraphqlService
		}),
		GraphqlModule,
		DishModule,
		SiteModule
	]
})
export class AppModule {}

this is our graphql service with all required configuration which we can play with

import { Injectable } from '@nestjs/common'
import { GqlOptionsFactory, GqlModuleOptions } from '@nestjs/graphql'
import { UserService } from '../../modules/user/user.service'
import { GraphQLError } from 'graphql'
import { join } from 'path'


@Injectable()
export class GraphqlService implements GqlOptionsFactory {
	constructor(private readonly userService: UserService) {}

	async createGqlOptions(): Promise<GqlModuleOptions> {
		const directiveResolvers = {
			isAuthenticated: (next, source, args, ctx) => {
				const { currentUser } = ctx

				if (!currentUser) {
					throw new Error('You are not authenticated!')
				}

				return next()
			},
			hasRole: (next, source, args, ctx) => {
				const { role } = args
				const { currentUser } = ctx

				if (!currentUser) {
					throw new Error('You are not authenticated!')
				}

				if (role !== currentUser.role) {
					throw new Error(
						`Must have role: ${role}, you have role: ${currentUser.role}`
					)
				}
				return next()
			}
		}

		return {
			typePaths: ['./**/*.graphql'],
			definitions: {
				path: join(process.cwd(), 'src/graphql.ts'),
				outputAs: 'class'
			},
			directiveResolvers,
			context: async ({ req, res, connection }) => {
				if (connection) {
					return {
						req: connection.context,
					}
				}

				let currentUser = ''
				const { token } = req.headers
				if (token) {
					currentUser = await this.userService.findOneByToken(token)
				}
				return {
					req,
					res,
					pubSub,
					currentUser
				}
			},
			formatError: err => {
				// console.log(err)
				return err
			},
			formatResponse: err => {
				// console.log(err)
				return err
			},
			debug: false,
			subscriptions: {
				onConnect: (connectionParams, webSocket, context) => {
					console.log('🔗 Connected to websocket')
				}
			},
			persistedQueries: {
				cache: new MemcachedCache(
					['memcached-server-1', 'memcached-server-2', 'memcached-server-3'],
					{ retries: 10, retry: 10000 } // Options
				)
			},
			installSubscriptionHandlers: true,
			introspection: true,
			playground: {
				settings: {
					'editor.cursorShape': 'line', // possible values: 'line', 'block', 'underline'
					'editor.fontFamily': `'Source Code Pro', 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace`,
					'editor.fontSize': 14,
					'editor.reuseHeaders': true, // new tab reuses headers from last tab
					'editor.theme': 'dark', // possible values: 'dark', 'light'
					'general.betaUpdates': false,
					'queryPlan.hideQueryPlanResponse': false,
					'request.credentials': 'include', // possible values: 'omit', 'include', 'same-origin'
					'tracing.hideTracingResponse': true
				}
			}
		}
	}
}

This service talks about lot of different things

  • Checking role for each and every query [auth and authz for apis]
  • managing cache for queries
  • passing data connection and logged In user data in all query and mutations
  • all playground options

Important part of this Service is what it returns

return {
			typePaths: ['./**/*.graphql'],
			definitions: {
				path: join(process.cwd(), 'src/graphql.ts'),
				outputAs: 'class'
			},
			directiveResolvers,
			context:  {}
}

To persist queries we are using MemcachedCache or Elastic search can be used

			persistedQueries: {
				cache: new MemcachedCache(
					['memcached-server-1', 'memcached-server-2', 'memcached-server-3'],
					{ retries: 10, retry: 10000 } // Options
				)
			},

CRUD with object User

Now we can define our modules having resolvers and services like user module for User CRUD

import { Module, Global } from '@nestjs/common'
import { UserResolver } from './user.resolver'
import { UserService } from './user.service'
import { TypeOrmModule } from '@nestjs/typeorm'
import { User } from './user.entity'

@Global()
@Module({
	imports: [TypeOrmModule.forFeature([User])],
	providers: [UserResolver, UserService],
	exports: [UserService]
})
export class UserModule {}

This is how our resolves look like and every module now can have their own .graphql type definitions file file

type User {
	_id: String!
	username: String!
	password: String!
	email: String!
	role: RoleEnum!
	status: Boolean!
	createdAt: String!
	updatedAt: String!
}

type Query {
	hello: String!
	me: User @isAuthenticated
	users(offset: Int!, limit: Int!): [User!] @isAuthenticated
	user(_id: String!): User @isAuthenticated
}

type Mutation {
	register(input: CreateUserInput!): User
	updateUser(_id: String!, input: UpdateUserInput!): Boolean
	deleteUser(_id: String!): Boolean @hasRole(role: "admin")
	deleteUsers: Boolean! @hasRole(role: "admin")
	login(input: LoginUserInput!): LoginResponse
	setRole(_id: String!, role: RoleEnum!): Boolean @isAuthenticated
}

type Subscription {
	userCreated: User
}

Resolver for User APIs with Query and Mutations

import { UpdateUserInput } from '../../graphql'

@Resolver('User')
export class UserResolver {
	constructor(private readonly userService: UserService) {}

	@Query(() => String)
	async hello() {
		return await 'world'
	}

	@Query(() => User)
	async me(@Context('currentUser') currentUser: User) {
		return await currentUser
	}

	@Query(() => [User])
	async users(@Args('offset') offset: number, @Args('limit') limit: number) {
		return this.userService.findAll(offset, limit)
	}

Our Simple services will fetch data from ORM layer

import { Injectable } from '@nestjs/common';
import { UserInput } from './user.input';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UserRepository } from 'typeorm';
import * as uuid from 'uuid';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: UserRepository<User>,
  ) {}

  async findAll(): Promise<User[]> {
    return this.userRepository.find();
  }

  async create(input: UserInput): Promise<User> {
    const user = new User();
    user._id = uuid.v4();
    user.username = input.username;
    user.password = input.password;
    return this.userRepository.save(user);
  }
}

Comments