Environment Setup and Basic Configuration
Installing Node.js and npm
- Node.js: Install the latest stable version.
- npm: Included with Node.js.
Initializing the Project
mkdir nestjs-graphql-app
cd nestjs-graphql-app
npm init -yInstalling the NestJS CLI
npm install -g @nestjs/cliCreating a New Project
nest new .Installing GraphQL Dependencies
npm install apollo-server-core graphql @nestjs/graphqlConfiguring GraphQL
Enable the GraphQL module in main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { GraphQLModule } from '@nestjs/graphql';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable GraphQL
app.useGlobalPipes(new GraphQLModule({
autoSchemaFile: 'schema.gql',
}));
await app.listen(3000);
}
bootstrap();Defining the GraphQL Schema
Creating the Schema File
Define the basic schema structure in src/graphql/schema.graphql:
type Query {
hello: String!
}
type Mutation {
createPost(title: String!, content: String!): Post!
}
type Post {
id: ID!
title: String!
content: String!
}Implementing Resolvers
Creating a Resolver
Use the CLI to create a resolver:
nest generate resolver postWriting Resolver Logic
Implement the logic in post.resolver.ts:
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { Post } from './post.entity';
@Resolver(() => Post)
export class PostResolver {
@Query(() => String)
hello() {
return 'Hello world!';
}
@Mutation(() => Post)
createPost(@Args('title') title: string, @Args('content') content: string) {
return { id: '1', title, content };
}
}Configuring TypeORM (Optional)
Installing TypeORM
npm install typeorm reflect-metadata mysqlConfiguring Database Connection
Add database configuration in app.module.ts:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Post } from './post.entity';
import { PostResolver } from './post.resolver';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'testdb',
entities: [Post],
synchronize: true,
}),
GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql',
}),
],
controllers: [AppController],
providers: [AppService, PostResolver],
})
export class AppModule {}Creating Entity Models
Creating the Post Entity
Define the entity class in src/post.entity.ts:
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
content: string;
}Data Persistence
Updating the Resolver
Update the createPost method in post.resolver.ts to persist data:
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { Post } from './post.entity';
import { getRepository } from 'typeorm';
@Resolver(() => Post)
export class PostResolver {
@Mutation(() => Post)
async createPost(@Args('title') title: string, @Args('content') content: string) {
const postRepository = getRepository(Post);
const post = postRepository.create({ title, content });
return postRepository.save(post);
}
}Custom Types
Defining custom types in the GraphQL schema can improve code readability and reusability. For example, define an Author type and use it in the Post type.
Update schema.graphql
type Author {
id: ID!
name: String!
}
type Post {
id: ID!
title: String!
content: String!
author: Author!
}Implementing Custom Types
Implement the author field in post.resolver.ts:
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { Post } from './post.entity';
import { Author } from './author.entity';
import { getRepository } from 'typeorm';
@Resolver(() => Post)
export class PostResolver {
@Mutation(() => Post)
async createPost(@Args('title') title: string, @Args('content') content: string, @Args('authorId') authorId: number) {
const postRepository = getRepository(Post);
const authorRepository = getRepository(Author);
const author = await authorRepository.findOne(authorId);
if (!author) throw new Error('Author not found');
const post = postRepository.create({ title, content, author });
return postRepository.save(post);
}
}Middleware
Defining Middleware
Middleware can handle common logic such as logging or permission validation.
Creating Middleware
nest generate middleware loggingImplementing Middleware
Implement logging functionality in logging.middleware.ts:
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`Request: ${req.method} ${req.url}`);
next();
}
}Applying Middleware
Apply the middleware globally in app.module.ts:
import { Module, MiddlewareConsumer } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Post } from './post.entity';
import { PostResolver } from './post.resolver';
import { Author } from './author.entity';
import { LoggingMiddleware } from './logging.middleware';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'testdb',
entities: [Post, Author],
synchronize: true,
}),
GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql',
context: ({ req }) => ({ req }),
}),
],
controllers: [AppController],
providers: [AppService, PostResolver],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggingMiddleware).forRoutes('*');
}
}Error Handling
Defining Error Handling
In GraphQL, errors can be handled by throwing exceptions.
Update the createPost method in post.resolver.ts to handle cases where the author does not exist:
Updating Resolver
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { Post } from './post.entity';
import { Author } from './author.entity';
import { getRepository } from 'typeorm';
@Resolver(() => Post)
export class PostResolver {
@Mutation(() => Post)
async createPost(@Args('title') title: string, @Args('content') content: string, @Args('authorId') authorId: number) {
const postRepository = getRepository(Post);
const authorRepository = getRepository(Author);
const author = await authorRepository.findOne(authorId);
if (!author) throw new Error('Author not found');
const post = postRepository.create({ title, content, author });
return postRepository.save(post);
}
}Global Error Handling
Add a global error handler in app.module.ts:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Post } from './post.entity';
import { PostResolver } from './post.resolver';
import { Author } from './author.entity';
import { LoggingMiddleware } from './logging.middleware';
import { AllExceptionsFilter } from './all-exceptions.filter';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'testdb',
entities: [Post, Author],
synchronize: true,
}),
GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql',
context: ({ req }) => ({ req }),
}),
],
controllers: [AppController],
providers: [AppService, PostResolver, AllExceptionsFilter],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggingMiddleware).forRoutes('*');
}
}Creating the Error Handler
nest generate filter all-exceptionsImplementing the Error Handler
Implement error handling logic in all-exceptions.filter.ts:
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status = 500;
let message = 'Internal server error';
if (exception instanceof HttpException) {
status = exception.getStatus();
message = exception.message;
} else if (typeof exception === 'string') {
message = exception;
}
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
error: message,
});
}
}Performance Optimization
Paginated Queries
GraphQL supports paginated queries to reduce data transfer and improve response speed.
Update post.resolver.ts to support paginated queries:
Updating Resolver
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { Post } from './post.entity';
import { getRepository } from 'typeorm';
import { Int } from '@nestjs/graphql';
@Resolver(() => Post)
export class PostResolver {
@Query(() => [Post])
async posts(@Args({ name: 'first', type: () => Int, defaultValue: 10 }) first: number) {
const postRepository = getRepository(Post);
return postRepository.find({ take: first });
}
}Lazy Loading
Lazy loading prevents loading large amounts of data at once, improving performance. Use the @Field() decorator from @nestjs/graphql to implement lazy loading.
Updating Resolver
import { Resolver, Query, Mutation, Args, Field, Root } from '@nestjs/graphql';
import { Post } from './post.entity';
import { Author } from './author.entity';
import { getRepository } from 'typeorm';
import { Int } from '@nestjs/graphql';
@Resolver(() => Post)
export class PostResolver {
@Query(() => [Post])
async posts(@Args({ name: 'first', type: () => Int, defaultValue: 10 }) first: number) {
const postRepository = getRepository(Post);
return postRepository.find({ take: first });
}
}
@Resolver(() => Post)
export class PostFieldResolver {
@Field(() => Author, { nullable: true })
async author(@Root() post: Post) {
const authorRepository = getRepository(Author);
return authorRepository.findOne(post.author.id);
}
}Using Dataloader
Dataloader is a technique for optimizing batch data loading, significantly reducing database queries.
Installing apollo-dataloader:
npm install apollo-dataloaderUpdating Resolver
import DataLoader from 'dataloader';
import { Resolver, Query, Mutation, Args, FieldResolver, Root } from '@nestjs/graphql';
import { Post } from './post.entity';
import { Author } from './author.entity';
import { getRepository } from 'typeorm';
import { Int } from '@nestjs/graphql';
@Resolver(() => Post)
export class PostResolver {
@Query(() => [Post])
async posts(@Args({ name: 'first', type: () => Int, defaultValue: 10 }) first: number) {
const postRepository = getRepository(Post);
return postRepository.find({ take: first });
}
}
@Resolver(() => Post)
export class PostFieldResolver {
private authorLoader: DataLoader<number, Author>;
constructor() {
this.authorLoader = new DataLoader(async (ids) => {
const authorRepository = getRepository(Author);
const authors = await authorRepository.findByIds(ids);
return ids.map((id) => authors.find((author) => author.id === id));
});
}
@Field(() => Author, { nullable: true })
async author(@Root() post: Post) {
return this.authorLoader.load(post.author.id);
}
}Benefits of Using Dataloader
- Reduced Database Queries: Combines multiple requests into a single batch request, reducing database access.
- Improved Performance: Decreases network latency and database load, enhancing overall performance.
Authorization and Authentication
Adding JWT Authentication
Using JSON Web Tokens (JWT) for user authentication is a common practice.
Installing JWT Libraries:
npm install jsonwebtoken @nestjs/jwtCreating the JWT Module
nest generate module jwtImplementing the JWT Module
Configure the JWT strategy in jwt.module.ts:
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
JwtModule.register({
secret: 'secretKey',
signOptions: { expiresIn: '60s' },
}),
],
providers: [JwtStrategy],
exports: [JwtModule],
})
export class JwtModule {}Creating the JWT Strategy
nest generate strategy jwtImplementing the JWT Strategy
Implement JWT validation logic in jwt.strategy.ts:
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { User } from './user.entity';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'secretKey',
});
}
async validate(payload: any) {
const user = await User.findOne({ where: { id: payload.sub } });
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}Using a Guard to Protect Routes
Create a JWT-based guard to protect GraphQL endpoints.
Creating the Guard
nest generate guard jwtImplementing the Guard
Implement JWT validation logic in jwt.guard.ts:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const gqlContext = GqlExecutionContext.create(context).getContext();
return new AuthGuard('jwt').canActivate(gqlContext);
}
}Applying the Guard
Apply the guard in app.module.ts:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Post } from './post.entity';
import { PostResolver } from './post.resolver';
import { Author } from './author.entity';
import { LoggingMiddleware } from './logging.middleware';
import { AllExceptionsFilter } from './all-exceptions.filter';
import { JwtModule } from './jwt/jwt.module';
import { JwtAuthGuard } from './jwt/jwt.guard';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'testdb',
entities: [Post, Author],
synchronize: true,
}),
GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql',
context: ({ req }) => ({ req }),
guards: [JwtAuthGuard],
}),
JwtModule,
],
controllers: [AppController],
providers: [AppService, PostResolver, AllExceptionsFilter],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggingMiddleware).forRoutes('*');
}
}



