Skip to content

Repository files navigation

NestJS GraphQL Authentication API

A complete, production-ready NestJS application with GraphQL, TypeORM, PostgreSQL, JWT authentication, and role-based access control.

✨ Features

  • πŸ” Authentication & Authorization

    • User registration and login
    • JWT-based authentication
    • Password hashing with bcrypt
    • Role-based access control (Admin/User)
    • Protected routes with Guards
  • πŸ“Š GraphQL API

    • Code-first approach
    • Auto-generated GraphQL schema
    • GraphQL Playground for testing
    • Proper error handling
  • πŸ’Ύ Database

    • PostgreSQL with TypeORM
    • Entity relationships
    • Database migrations support
    • Seed data for development
  • πŸ“¦ Posts Management (CRUD)

    • Create, Read, Update, Delete operations
    • Pagination support
    • Filtering and search
    • Authorization for modifications
  • πŸ§ͺ Testing

    • Unit tests for services and resolvers
    • Jest testing framework
    • High test coverage

πŸš€ Getting Started

Prerequisites

  • Node.js (v18 or higher)
  • PostgreSQL (v12 or higher)
  • npm or yarn

Installation

  1. Clone the repository

    git clone <repository-url>
    cd nestjs-graphql-auth
  2. Install dependencies

    npm install
  3. Configure environment variables

    cp .env.example .env
    # Edit .env with your database credentials
  4. Set up PostgreSQL database

    CREATE DATABASE nestjs_graphql_auth;
  5. Run database seeds (optional)

    npm run seed
  6. Start the development server

    npm run start:dev
  7. Access the application

πŸ“ Project Structure

src/
β”œβ”€β”€ auth/                   # Authentication module
β”‚   β”œβ”€β”€ dto/               # Data Transfer Objects
β”‚   β”œβ”€β”€ strategies/        # Passport JWT strategy
β”‚   β”œβ”€β”€ auth.module.ts
β”‚   β”œβ”€β”€ auth.resolver.ts
β”‚   └── auth.service.ts
β”œβ”€β”€ users/                 # Users module
β”‚   β”œβ”€β”€ dto/
β”‚   β”œβ”€β”€ entities/
β”‚   β”œβ”€β”€ users.module.ts
β”‚   β”œβ”€β”€ users.resolver.ts
β”‚   └── users.service.ts
β”œβ”€β”€ posts/                 # Posts module
β”‚   β”œβ”€β”€ dto/
β”‚   β”œβ”€β”€ entities/
β”‚   β”œβ”€β”€ posts.module.ts
β”‚   β”œβ”€β”€ posts.resolver.ts
β”‚   └── posts.service.ts
β”œβ”€β”€ common/                # Shared resources
β”‚   β”œβ”€β”€ decorators/        # Custom decorators
β”‚   β”œβ”€β”€ guards/            # Auth & Role guards
β”‚   β”œβ”€β”€ filters/           # Exception filters
β”‚   └── interceptors/      # Logging interceptor
β”œβ”€β”€ config/                # Configuration files
β”œβ”€β”€ database/              # Database configuration
β”œβ”€β”€ seeds/                 # Seed data
β”œβ”€β”€ app.module.ts          # Root module
└── main.ts               # Application entry point

πŸ”‘ Authentication

Register

mutation {
  register(registerInput: {
    email: "user@example.com"
    firstName: "John"
    lastName: "Doe"
    password: "password123"
  }) {
    accessToken
    refreshToken
    user {
      id
      email
      firstName
      lastName
      role
    }
  }
}

Login

mutation {
  login(loginInput: {
    email: "user@example.com"
    password: "password123"
  }) {
    accessToken
    refreshToken
    user {
      id
      email
      firstName
      lastName
      role
    }
  }
}

πŸ”‘ Next: Copy the accessToken from the response and add it to HTTP Headers to make authenticated requests.


πŸ”‘ How to Use JWT Token in GraphQL Playground

After logging in or registering, you'll receive an accessToken. To make authenticated requests, you need to add this token to the HTTP Headers in GraphQL Playground.

Step 1: Login to Get Token

mutation {
  login(loginInput: {
    email: "admin@example.com"
    password: "admin123"
  }) {
    accessToken
    user {
      id
      email
      role
    }
  }
}

Step 2: Add Token to HTTP Headers

Click on "HTTP HEADERS" tab at the bottom of GraphQL Playground and add:

{
  "Authorization": "Bearer YOUR_ACCESS_TOKEN_HERE"
}

Example:

{
  "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

πŸ’‘ Tip: The token is automatically decoded by the server to identify the current user.

Step 3: Make Authenticated Queries

Now you can call protected queries and mutations:

# Get current logged-in user info
query {
  me {
    id
    email
    firstName
    lastName
    role
  }
}

# Create a new post (requires authentication)
mutation {
  createPost(createPostInput: {
    title: "My First Post"
    content: "This is my post content"
    published: true
  }) {
    id
    title
    content
    createdAt
  }
}

# Get my posts
query {
  myPosts(paginationInput: { page: 1, limit: 10 }) {
    items {
      id
      title
      published
    }
  }
}

πŸ“Š GraphQL Queries & Mutations

Users

Get All Users (Paginated)

query {
  users(paginationInput: { page: 1, limit: 10 }) {
    items {
      id
      email
      firstName
      lastName
      role
      createdAt
    }
    total
    page
    limit
    totalPages
    hasNextPage
    hasPreviousPage
  }
}

Get User by ID

query {
  user(id: "user-id") {
    id
    email
    firstName
    lastName
    role
    posts {
      id
      title
      published
    }
  }
}

Get Current User

query {
  me {
    id
    email
    firstName
    lastName
    role
  }
}

Create User (Admin only)

mutation {
  createUser(createUserInput: {
    email: "newuser@example.com"
    firstName: "New"
    lastName: "User"
    password: "password123"
    role: USER
  }) {
    id
    email
    firstName
    lastName
    role
  }
}

Update User

mutation {
  updateUser(
    id: "user-id"
    updateUserInput: {
      firstName: "Updated"
      lastName: "Name"
    }
  ) {
    id
    email
    firstName
    lastName
  }
}

Delete User (Admin only)

mutation {
  removeUser(id: "user-id")
}

Posts

Get All Posts (Paginated & Filtered)

query {
  posts(
    paginationInput: { page: 1, limit: 10 }
    filterInput: { published: true, searchTerm: "GraphQL" }
  ) {
    items {
      id
      title
      content
      published
      createdAt
      author {
        id
        firstName
        lastName
      }
    }
    total
    page
    limit
    totalPages
    hasNextPage
    hasPreviousPage
  }
}

Get Post by ID

query {
  post(id: "post-id") {
    id
    title
    content
    published
    createdAt
    updatedAt
    author {
      id
      firstName
      lastName
    }
  }
}

Get My Posts

query {
  myPosts(paginationInput: { page: 1, limit: 10 }) {
    items {
      id
      title
      content
      published
      createdAt
    }
    total
    page
    limit
  }
}

Get Posts by User

query {
  postsByUser(
    userId: "user-id"
    paginationInput: { page: 1, limit: 10 }
  ) {
    items {
      id
      title
      content
      published
      author {
        firstName
        lastName
      }
    }
    total
  }
}

Create Post (Authenticated)

mutation {
  createPost(createPostInput: {
    title: "My New Post"
    content: "This is the content of my post."
    published: true
  }) {
    id
    title
    content
    published
    createdAt
    author {
      id
      firstName
      lastName
    }
  }
}

Update Post (Author or Admin)

mutation {
  updatePost(
    id: "post-id"
    updatePostInput: {
      title: "Updated Title"
      content: "Updated content"
    }
  ) {
    id
    title
    content
    updatedAt
  }
}

Delete Post (Author or Admin)

mutation {
  deletePost(id: "post-id")
}

Publish/Unpublish Post

mutation {
  publishPost(id: "post-id") {
    id
    title
    published
  }
}

mutation {
  unpublishPost(id: "post-id") {
    id
    title
    published
  }
}

πŸ›‘οΈ Role-Based Access Control

Roles

  • ADMIN: Full access to all resources
  • USER: Limited access, can only manage own resources

Permissions Matrix

Operation Admin User
Create Post βœ… βœ…
Read All Posts βœ… βœ…
Read Own Posts βœ… βœ…
Update Any Post βœ… ❌
Update Own Post βœ… βœ…
Delete Any Post βœ… ❌
Delete Own Post βœ… βœ…
Create User βœ… ❌
Read All Users βœ… βœ…
Update Any User βœ… ❌
Update Own Profile βœ… βœ…
Delete Any User βœ… ❌
Delete Own Account ❌ ❌

πŸ§ͺ Testing

Run Tests

# Unit tests
npm test

# Watch mode
npm run test:watch

# Coverage report
npm run test:cov

# E2E tests
npm run test:e2e

Test Coverage

  • Auth Service
  • Auth Resolver
  • Users Service
  • Users Resolver
  • Posts Service
  • Posts Resolver

🌱 Seed Data

Default users created by seed script:

Email Password Role
admin@example.com admin123 ADMIN
john@example.com password123 USER
jane@example.com password123 USER
bob@example.com password123 USER

Run seeds:

npm run seed

πŸ”§ Environment Variables

Variable Description Default
DB_HOST PostgreSQL host localhost
DB_PORT PostgreSQL port 5432
DB_USERNAME Database username postgres
DB_PASSWORD Database password password
DB_DATABASE Database name nestjs_graphql_auth
PORT Application port 3000
JWT_SECRET JWT secret key -
JWT_EXPIRATION JWT expiration time 1d
GRAPHQL_PLAYGROUND Enable GraphQL Playground true
GRAPHQL_DEBUG Enable GraphQL debug mode true
GRAPHQL_INTROSPECTION Enable schema introspection true

πŸ“œ Available Scripts

# Development
npm run start:dev          # Start with hot reload

# Production
npm run build              # Build application
npm run start:prod         # Start production build

# Testing
npm test                   # Run unit tests
npm run test:watch         # Run tests in watch mode
npm run test:cov           # Generate coverage report
npm run test:e2e           # Run E2E tests

# Database
npm run seed               # Run database seeds
npm run migration:generate # Generate migration
npm run migration:run      # Run migrations
npm run migration:revert   # Revert last migration

# Linting & Formatting
npm run lint               # Run ESLint
npm run format             # Format with Prettier

πŸ”’ Security Best Practices

  • βœ… Password hashing with bcrypt
  • βœ… JWT-based authentication
  • βœ… Input validation with class-validator
  • βœ… SQL injection protection via TypeORM
  • βœ… Role-based access control
  • βœ… Environment variable configuration
  • βœ… CORS enabled

πŸ“– Complete NestJS Concepts Guide

This section provides a comprehensive guide to NestJS concepts used in this project, from fundamentals to advanced topics.


πŸ“š Table of Contents

  1. NestJS Architecture Overview
  2. Modules
  3. Controllers vs Resolvers
  4. Services & Providers
  5. Dependency Injection
  6. Middleware
  7. Guards
  8. Interceptors
  9. Exception Filters
  10. Pipes
  11. Custom Decorators
  12. Configuration Management
  13. Production Best Practices

1. NestJS Architecture Overview

NestJS is a progressive Node.js framework built with TypeScript that uses modern JavaScript and implements design patterns like Dependency Injection, SOLID principles, and Object-Oriented Programming.

Core Architecture Components

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Client Request                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Middleware β†’ Guards β†’ Interceptors β†’ Pipes β†’ Handler   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Controller/Resolver                    β”‚
β”‚                   (Request Handler)                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Service/Provider                      β”‚
β”‚                  (Business Logic)                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Repository/Database                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Concepts:

  • Modular Design: Application is divided into modules
  • Providers: Injectable classes (services, repositories)
  • Controllers/Resolvers: Handle incoming requests
  • Middleware: Process requests before they reach handlers
  • Guards: Control access to routes
  • Interceptors: Transform request/response flow
  • Pipes: Validate and transform data

2. Modules

Modules are the fundamental building blocks of a NestJS application. They organize related components (controllers, services, etc.) together.

Module Structure

// app.module.ts
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';

@Module({
  imports: [AuthModule, UsersModule],     // Other modules to import
  controllers: [AppController],            // Controllers (REST)
  providers: [AppService],                 // Services, Repositories
  exports: [AppService],                   // Providers to export
})
export class AppModule {}

Module Types

Type Description Example
Feature Module Encapsulates specific feature AuthModule, UsersModule
Shared Module Reusable across modules CommonModule
Global Module Available everywhere @Global() decorator
Dynamic Module Configured at runtime ConfigModule.forRoot()

Our Project's Module Hierarchy

AppModule (Root)
β”œβ”€β”€ ConfigModule (Global - env variables)
β”œβ”€β”€ TypeOrmModule (Database connection)
β”œβ”€β”€ GraphQLModule (GraphQL configuration)
β”œβ”€β”€ AuthModule
β”‚   β”œβ”€β”€ JwtModule
β”‚   β”œβ”€β”€ PassportModule
β”‚   └── User entity repository
β”œβ”€β”€ UsersModule
β”‚   └── User entity repository
└── PostsModule
    └── Post entity repository

Example - Feature Module:

// auth/auth.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([User]),  // Register User entity
    PassportModule,
    JwtModule.registerAsync({...}),    // Async configuration
  ],
  providers: [AuthService, AuthResolver, JwtStrategy],
  exports: [AuthService],              // Make available to other modules
})
export class AuthModule {}

3. Services & Providers

Providers are the core of NestJS. They can be injected as dependencies. The most common type is a Service.

What is a Provider?

A provider is any class annotated with @Injectable() that can be injected into other classes.

@Injectable()
export class AuthService {
  // Business logic here
}

Service Pattern

Services contain business logic and are separated from controllers/resolvers:

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
    private readonly jwtService: JwtService,
  ) {}

  async register(registerInput: RegisterInput): Promise<AuthResponse> {
    // 1. Check if user exists
    const existingUser = await this.userRepository.findOne({
      where: { email: registerInput.email },
    });

    if (existingUser) {
      throw new ConflictException('Email already registered');
    }

    // 2. Hash password
    const hashedPassword = await bcrypt.hash(registerInput.password, 10);

    // 3. Create and save user
    const user = this.userRepository.create({
      ...registerInput,
      password: hashedPassword,
    });
    
    return this.userRepository.save(user);
  }
}

Provider Registration

// Option 1: Short form
@Module({
  providers: [AuthService],
})

// Option 2: With custom token
@Module({
  providers: [
    {
      provide: 'AUTH_SERVICE',
      useClass: AuthService,
    },
  ],
})

// Option 3: Factory provider
@Module({
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: async (config: ConfigService) => {
        return createConnection(config.get('database'));
      },
      inject: [ConfigService],
    },
  ],
})

4. Controllers vs Resolvers

REST Controllers (Traditional)

Controllers handle HTTP requests and return HTTP responses:

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll(): Promise<User[]> {
    return this.usersService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string): Promise<User> {
    return this.usersService.findOne(id);
  }

  @Post()
  create(@Body() createUserDto: CreateUserDto): Promise<User> {
    return this.usersService.create(createUserDto);
  }
}

GraphQL Resolvers (This Project)

Resolvers handle GraphQL operations (queries, mutations, subscriptions):

@Resolver(() => User)
export class UsersResolver {
  constructor(private readonly usersService: UsersService) {}

  // Query - fetching data
  @Query(() => [User], { name: 'users' })
  findAll() {
    return this.usersService.findAll();
  }

  // Query with parameters
  @Query(() => User, { name: 'user' })
  findOne(@Args('id') id: string) {
    return this.usersService.findOne(id);
  }

  // Mutation - modifying data
  @Mutation(() => User)
  createUser(@Args('createUserInput') createUserInput: CreateUserInput) {
    return this.usersService.create(createUserInput);
  }
}

Comparison

Aspect REST Controllers GraphQL Resolvers
Protocol HTTP GraphQL over HTTP
Operations GET, POST, PUT, DELETE Query, Mutation, Subscription
Data Shape Fixed response structure Client specifies exact fields
Endpoints Multiple URLs Single endpoint (/graphql)
Decorators @Get(), @Post() @Query(), @Mutation()

5. Dependency Injection

Dependency Injection (DI) is a design pattern where classes receive their dependencies from external sources rather than creating them internally.

Why Use DI?

  1. Loose Coupling: Classes don't depend on concrete implementations
  2. Testability: Easy to mock dependencies in tests
  3. Reusability: Same service can be used in multiple places
  4. Maintainability: Changes in one place don't break others

How DI Works in NestJS

@Injectable()
export class AuthService {
  // Dependencies are injected automatically
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
    private readonly jwtService: JwtService,
  ) {}
}

NestJS handles the instantiation:

1. Creates UserRepository instance
2. Creates JwtService instance  
3. Creates AuthService instance with above dependencies

Injection Patterns

Constructor Injection (Recommended):

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

Property Injection:

@Injectable()
export class UsersService {
  @InjectRepository(User)
  private userRepository: Repository<User>;
}

Custom Provider Injection:

@Injectable()
export class MyService {
  constructor(
    @Inject('CUSTOM_TOKEN')
    private customService: CustomService,
  ) {}
}

DI Container Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Application Bootstrap             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
                    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Parse Module Dependencies            β”‚
β”‚  (Build dependency graph)                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
                    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Resolve Dependencies                 β”‚
β”‚  (Create instances in order)              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    β”‚
                    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Inject into Constructors             β”‚
β”‚  (Pass instances to classes)              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

6. Middleware

Middleware is a function that executes before the route handler. It has access to the request and response objects.

When to Use Middleware

  • Logging/Analytics
  • Authentication check
  • CORS handling
  • Request parsing
  • Rate limiting

Creating Middleware

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  private logger = new Logger('HTTP');

  use(req: Request, res: Response, next: NextFunction) {
    const { method, originalUrl } = req;
    const start = Date.now();

    res.on('finish', () => {
      const duration = Date.now() - start;
      this.logger.log(`${method} ${originalUrl} - ${res.statusCode} - ${duration}ms`);
    });

    next(); // Pass control to next middleware/handler
  }
}

Applying Middleware

// app.module.ts
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes({ path: '*', method: RequestMethod.ALL }); // Apply to all routes
      // .forRoutes(UsersController); // Apply to specific controller
      // .exclude({ path: 'auth/login', method: RequestMethod.POST }) // Exclude paths
  }
}

Functional Middleware

For simple cases, use functions:

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`Request...`);
  next();
}

// Apply
consumer.apply(logger).forRoutes('*');

7. Guards

Guards determine whether a request should be handled by the route handler. They implement authorization logic.

Guard Execution Flow

Request β†’ Middleware β†’ Guards β†’ Interceptors β†’ Pipes β†’ Handler
              β”‚
              β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚   Guard     β”‚ ──No──→ Throw Exception (401/403)
        β”‚ (canActivate)β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚Yes
              β–Ό
        Continue to Handler

Creating a Guard

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // Get required roles from metadata
    const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(
      ROLES_KEY,
      [context.getHandler(), context.getClass()],
    );

    if (!requiredRoles) {
      return true; // No roles required, allow access
    }

    // Get user from request
    const ctx = GqlExecutionContext.create(context);
    const { user } = ctx.getContext().req;

    // Check if user has required role
    const hasRole = requiredRoles.some((role) => user.role === role);
    
    if (!hasRole) {
      throw new ForbiddenException('Insufficient permissions');
    }

    return true;
  }
}

Using Guards

Method-level:

@Resolver(() => Post)
export class PostsResolver {
  
  @Mutation(() => Post)
  @UseGuards(RolesGuard)
  @Roles(UserRole.ADMIN)  // Custom decorator
  deletePost(@Args('id') id: string) {
    return this.postsService.remove(id);
  }
}

Controller/Resolver-level:

@Resolver(() => Post)
@UseGuards(JwtAuthGuard)  // All methods protected
export class PostsResolver { }

Global-level:

// app.module.ts
@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard,  // Applies to all routes
    },
  ],
})

Our Project's Guards

Guard Purpose Usage
JwtAuthGuard Validate JWT token Global + specific routes
RolesGuard Check user roles Combined with @Roles() decorator

8. Interceptors

Interceptors intercept incoming requests and outgoing responses. They can:

  • Transform responses
  • Add extra logic before/after method execution
  • Cache responses
  • Handle timeouts
  • Log requests

Interceptor Interface

export interface NestInterceptor<T = any, R = any> {
  intercept(context: ExecutionContext, next: CallHandler<T>): Observable<R> | Promise<Observable<R>>;
}

Creating an Interceptor

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(LoggingInterceptor.name);

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const ctx = GqlExecutionContext.create(context);
    const info = ctx.getInfo();
    const start = Date.now();

    // Code executed BEFORE handler
    this.logger.log(`Incoming: ${info.parentType.name}.${info.fieldName}`);

    return next.handle().pipe(
      // Code executed AFTER handler
      tap((data) => {
        const duration = Date.now() - start;
        this.logger.log(`${info.fieldName} completed in ${duration}ms`);
      }),
    );
  }
}

Using Interceptors

Method-level:

@Query(() => User)
@UseInterceptors(LoggingInterceptor, TransformInterceptor)
findOne(@Args('id') id: string) {
  return this.usersService.findOne(id);
}

Controller/Resolver-level:

@Resolver(() => User)
@UseInterceptors(LoggingInterceptor)
export class UsersResolver { }

Global-level:

// app.module.ts
@Module({
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
  ],
})

Transform Interceptor Example

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    return next.handle().pipe(
      map((data) => ({
        data,
        timestamp: new Date().toISOString(),
        status: 'success',
      })),
    );
  }
}

9. Exception Filters

Exception Filters handle exceptions thrown by your application and format the response sent to the client.

Built-in HTTP Exceptions

throw new BadRequestException('Invalid input');
throw new UnauthorizedException('Please login');
throw new ForbiddenException('Access denied');
throw new NotFoundException('User not found');
throw new ConflictException('Email already exists');
throw new InternalServerErrorException('Something went wrong');

Creating Custom Exception Filter

@Catch()  // Catch all exceptions
export class GraphQLExceptionFilter implements GqlExceptionFilter {
  private readonly logger = new Logger(GraphQLExceptionFilter.name);

  catch(exception: any, host: ArgumentsHost) {
    const gqlHost = GqlArgumentsHost.create(host);
    const info = gqlHost.getInfo();

    // Determine error details
    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = 'Internal server error';
    let code = 'INTERNAL_SERVER_ERROR';

    if (exception instanceof HttpException) {
      status = exception.getStatus();
      message = exception.message;
      code = this.getErrorCode(status);
    }

    // Log the error
    this.logger.error(
      `${info.parentType.name}.${info.fieldName}: ${message}`,
      exception.stack,
    );

    // Return formatted error
    return {
      statusCode: status,
      message,
      code,
      timestamp: new Date().toISOString(),
      path: `${info.parentType.name}.${info.fieldName}`,
    };
  }

  private getErrorCode(status: number): string {
    switch (status) {
      case HttpStatus.UNAUTHORIZED: return 'UNAUTHORIZED';
      case HttpStatus.FORBIDDEN: return 'FORBIDDEN';
      case HttpStatus.NOT_FOUND: return 'NOT_FOUND';
      default: return 'INTERNAL_SERVER_ERROR';
    }
  }
}

Using Exception Filters

Method-level:

@Query(() => User)
@UseFilters(GraphQLExceptionFilter)
findOne(@Args('id') id: string) {
  return this.usersService.findOne(id);
}

Global-level:

// app.module.ts
@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: GraphQLExceptionFilter,
    },
  ],
})

10. Pipes

Pipes transform input data and validate it before it reaches the handler.

Built-in Pipes

Pipe Purpose
ValidationPipe Validate and transform DTOs
ParseIntPipe Parse string to integer
ParseBoolPipe Parse string to boolean
ParseArrayPipe Parse to array
DefaultValuePipe Set default value

Global Validation Pipe

// main.ts
app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,           // Strip properties without decorators
    forbidNonWhitelisted: true, // Throw error for extra properties
    transform: true,           // Transform to DTO instances
    transformOptions: {
      enableImplicitConversion: true,
    },
  }),
);

DTO with Validation

export class CreateUserInput {
  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  @Matches(/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
    message: 'Password must contain uppercase, lowercase, and number',
  })
  password: string;

  @IsString()
  @IsNotEmpty()
  firstName: string;

  @IsEnum(UserRole)
  @IsOptional()
  role?: UserRole;
}

Custom Pipe

@Injectable()
export class ParseObjectIdPipe implements PipeTransform<string, string> {
  transform(value: string): string {
    if (!isValidObjectId(value)) {
      throw new BadRequestException('Invalid ObjectId');
    }
    return value;
  }
}

// Usage
@Get(':id')
findOne(@Param('id', ParseObjectIdPipe) id: string) {
  return this.service.findOne(id);
}

11. Custom Decorators

Decorators add metadata to classes, methods, or parameters.

Creating Custom Decorators

Metadata Decorator:

// decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);

// Usage
@Roles(UserRole.ADMIN)
@Mutation(() => User)
deleteUser(@Args('id') id: string) { }

Parameter Decorator:

// decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const gqlCtx = GqlExecutionContext.create(ctx);
    return gqlCtx.getContext().req.user;
  },
);

// Usage
@Query(() => User)
me(@CurrentUser() user: User) {
  return user;
}

Method Decorator:

// decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

// Usage in Guard
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
  context.getHandler(),
  context.getClass(),
]);

if (isPublic) return true;

12. Configuration Management

NestJS provides the ConfigModule for environment-based configuration.

Setup

// app.module.ts
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,              // Available everywhere
      envFilePath: '.env',         // Load from .env file
      load: [appConfig, dbConfig], // Load configuration objects
    }),
  ],
})

Configuration Files

// config/app.config.ts
export default registerAs('app', () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  nodeEnv: process.env.NODE_ENV || 'development',
  graphql: {
    playground: process.env.GRAPHQL_PLAYGROUND === 'true',
    debug: process.env.GRAPHQL_DEBUG === 'true',
  },
}));

// config/database.config.ts
export default registerAs('database', () => ({
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT, 10) || 5432,
  username: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_DATABASE,
}));

Using Configuration

// Using ConfigService
@Injectable()
export class AppService {
  constructor(private configService: ConfigService) {}

  getPort(): number {
    return this.configService.get<number>('app.port');
    // Or: this.configService.get('PORT');
  }
}

// Async module configuration
TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (configService: ConfigService) => ({
    type: 'postgres',
    host: configService.get('database.host'),
    port: configService.get('database.port'),
    // ...
  }),
  inject: [ConfigService],
}),

13. Production Best Practices

Security Checklist

// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 1. Enable CORS with specific origins
  app.enableCors({
    origin: process.env.ALLOWED_ORIGINS?.split(',') || false,
    credentials: true,
  });

  // 2. Helmet for security headers
  app.use(helmet());

  // 3. Rate limiting
  app.use(
    rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100, // limit each IP to 100 requests per windowMs
    }),
  );

  // 4. Global validation
  app.useGlobalPipes(new ValidationPipe({ whitelist: true }));

  // 5. Graceful shutdown
  app.enableShutdownHooks();

  await app.listen(process.env.PORT || 3000);
}

Performance Optimization

// Enable compression
app.use(compression());

// Use fastify adapter for better performance
const app = await NestFactory.create<NestFastifyApplication>(
  AppModule,
  new FastifyAdapter(),
);

Logging

// Custom logger
const app = await NestFactory.create(AppModule, {
  logger: ['error', 'warn', 'log'],
});

// Winston for production
const app = await NestFactory.create(AppModule, {
  bufferLogs: true,
});
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));

Health Checks

// Install: npm install @nestjs/terminus
@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.db.pingCheck('database'),
    ]);
  }
}

Environment-Based Configuration

// app.module.ts
const isProduction = process.env.NODE_ENV === 'production';

@Module({
  imports: [
    GraphQLModule.forRoot({
      debug: !isProduction,
      playground: !isProduction,
      introspection: !isProduction,
    }),
  ],
})

πŸ“š Additional Resources

🀝 Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

πŸ“„ License

This project is licensed under the MIT License.

πŸ’‘ Support

If you encounter any issues or have questions:

  1. Check the documentation above
  2. Review the code comments
  3. Open an issue in the repository

About

A complete, easy-to-understand manual covering NestJS fundamentals, advanced concepts, architecture, modules, services, controllers, middleware, guards, and production best practices.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages