NestJS
TypeScript-first Node.js framework. Angular-inspired architecture enabling scalable server-side application development with dependency injection and decorators.
GitHub Overview
nestjs/nest
A progressive Node.js framework for building efficient, scalable, and enterprise-grade server-side applications with TypeScript/JavaScript 🚀
Topics
Star History
Framework
NestJS
Overview
NestJS is a TypeScript-first framework for building efficient and scalable Node.js server-side applications. It adopts an Angular-inspired architecture for enterprise-grade development.
Details
NestJS is a progressive Node.js web framework developed by Kamil Myśliwiec in 2017. With TypeScript as a first-class citizen and adopting Angular's architectural patterns, it specializes in enterprise-grade application development. Key features include declarative design using decorators (@Injectable, @Controller, @Module, etc.), dependency injection (DI) and IoC containers, modular architecture, powerful CLI tools, GraphQL/WebSocket/microservices support, and a rich library ecosystem. Built on Express.js or Fastify as a base, it excels in RESTful API, GraphQL API, microservices, and real-time application development. Particularly for large-scale team development and enterprise applications, its design philosophy emphasizes maintainability and scalability, making it suitable for long-term development projects through TypeScript's type safety and Angular-like structure.
Pros and Cons
Pros
- TypeScript First: Complete type safety and IntelliSense support
- Structured Architecture: Angular-like modular design
- Dependency Injection: Testable and maintainable code
- Rich Features: Built-in support for GraphQL, WebSocket, microservices
- Powerful CLI: Code generation and development efficiency
- Enterprise Ready: Design suitable for large-scale projects
- Rich Ecosystem: Integration with Passport, TypeORM, Mongoose, etc.
- Comprehensive Documentation: Detailed official documentation and samples
Cons
- High Learning Curve: Requires understanding of TypeScript, decorators, and DI
- Initial Setup Complexity: May be overkill for small projects
- Performance: Overhead compared to minimal frameworks
- Decorator Dependency: High dependency on experimental features
- Bundle Size: Relatively large library size
- Angular Knowledge Required: Takes time to understand without Angular experience
- Flexibility Limitations: Freedom may be restricted by strict structure
Key Links
- NestJS Official Website
- NestJS Official Documentation
- NestJS GitHub Repository
- NestJS CLI
- NestJS Community
- NestJS Slack
Code Examples
Hello World
// main.ts - Application entry point
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
console.log('NestJS application is running on http://localhost:3000');
}
bootstrap();
// app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Get('health')
getHealth() {
return {
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime()
};
}
}
// app.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello, NestJS!';
}
}
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Controllers and Routing
// users.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
ParseIntPipe,
ValidationPipe,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto, UpdateUserDto, UserResponseDto } from './dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
async findAll(
@Query('page', ParseIntPipe) page: number = 1,
@Query('limit', ParseIntPipe) limit: number = 10,
): Promise<UserResponseDto[]> {
return this.usersService.findAll(page, limit);
}
@Get(':id')
async findOne(
@Param('id', ParseIntPipe) id: number,
): Promise<UserResponseDto> {
return this.usersService.findOne(id);
}
@Post()
@HttpCode(HttpStatus.CREATED)
async create(
@Body(ValidationPipe) createUserDto: CreateUserDto,
): Promise<UserResponseDto> {
return this.usersService.create(createUserDto);
}
@Put(':id')
async update(
@Param('id', ParseIntPipe) id: number,
@Body(ValidationPipe) updateUserDto: UpdateUserDto,
): Promise<UserResponseDto> {
return this.usersService.update(id, updateUserDto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id', ParseIntPipe) id: number): Promise<void> {
return this.usersService.remove(id);
}
@Get('search/by-name')
async searchByName(
@Query('name') name: string,
): Promise<UserResponseDto[]> {
return this.usersService.searchByName(name);
}
}
// dto/create-user.dto.ts
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty()
@IsString()
name: string;
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
}
// dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
// dto/user-response.dto.ts
export class UserResponseDto {
id: number;
name: string;
email: string;
createdAt: Date;
updatedAt: Date;
}
Services and Dependency Injection
// users.service.ts
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto, UpdateUserDto, UserResponseDto } from './dto';
import * as bcrypt from 'bcrypt';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async findAll(page: number, limit: number): Promise<UserResponseDto[]> {
const [users, total] = await this.userRepository.findAndCount({
skip: (page - 1) * limit,
take: limit,
order: { createdAt: 'DESC' },
});
return users.map(user => this.toResponseDto(user));
}
async findOne(id: number): Promise<UserResponseDto> {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException('User not found');
}
return this.toResponseDto(user);
}
async create(createUserDto: CreateUserDto): Promise<UserResponseDto> {
const existingUser = await this.userRepository.findOne({
where: { email: createUserDto.email },
});
if (existingUser) {
throw new ConflictException('This email address is already in use');
}
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
const user = this.userRepository.create({
...createUserDto,
password: hashedPassword,
});
const savedUser = await this.userRepository.save(user);
return this.toResponseDto(savedUser);
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
const user = await this.findOne(id); // Existence check
if (updateUserDto.password) {
updateUserDto.password = await bcrypt.hash(updateUserDto.password, 10);
}
await this.userRepository.update(id, updateUserDto);
const updatedUser = await this.userRepository.findOne({ where: { id } });
return this.toResponseDto(updatedUser);
}
async remove(id: number): Promise<void> {
const user = await this.findOne(id); // Existence check
await this.userRepository.delete(id);
}
async searchByName(name: string): Promise<UserResponseDto[]> {
const users = await this.userRepository
.createQueryBuilder('user')
.where('user.name LIKE :name', { name: `%${name}%` })
.getMany();
return users.map(user => this.toResponseDto(user));
}
private toResponseDto(user: User): UserResponseDto {
const { password, ...result } = user;
return result;
}
}
// entities/user.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column()
password: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
Module System
// users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // Export for use in other modules
})
export class UsersModule {}
// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
UsersModule, // Import to use UsersService
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET || 'your-secret-key',
signOptions: { expiresIn: '7d' },
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import { User } from './users/entities/user.entity';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 5432,
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'password',
database: process.env.DB_NAME || 'nestjs_app',
entities: [User],
synchronize: process.env.NODE_ENV === 'development',
}),
UsersModule,
AuthModule,
],
})
export class AppModule {}
Authentication and Guards
// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
) {}
async validateUser(email: string, password: string): Promise<any> {
try {
const user = await this.usersService.findByEmail(email);
if (user && await bcrypt.compare(password, user.password)) {
const { password, ...result } = user;
return result;
}
return null;
} catch (error) {
return null;
}
}
async login(email: string, password: string) {
const user = await this.validateUser(email, password);
if (!user) {
throw new UnauthorizedException('Invalid email or password');
}
const payload = { email: user.email, sub: user.id };
return {
access_token: this.jwtService.sign(payload),
user,
};
}
}
// jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET || 'your-secret-key',
});
}
async validate(payload: any) {
return { userId: payload.sub, email: payload.email };
}
}
// auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// protected.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/auth.guard';
import { RolesGuard } from '../auth/roles.guard';
import { Roles } from '../auth/roles.decorator';
@Controller('protected')
@UseGuards(JwtAuthGuard)
export class ProtectedController {
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
@Get('admin')
@UseGuards(RolesGuard)
@Roles('admin')
getAdminData() {
return { message: 'Admin-only data' };
}
}
GraphQL Integration
// app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
context: ({ req }) => ({ req }),
}),
// Other modules
],
})
export class AppModule {}
// users.resolver.ts
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
import { CreateUserInput } from './dto/create-user.input';
import { JwtAuthGuard } from '../auth/auth.guard';
@Resolver(() => User)
export class UsersResolver {
constructor(private readonly usersService: UsersService) {}
@Query(() => [User], { name: 'users' })
@UseGuards(JwtAuthGuard)
findAll() {
return this.usersService.findAll(1, 10);
}
@Query(() => User, { name: 'user' })
@UseGuards(JwtAuthGuard)
findOne(@Args('id', { type: () => Int }) id: number) {
return this.usersService.findOne(id);
}
@Mutation(() => User)
createUser(@Args('createUserInput') createUserInput: CreateUserInput) {
return this.usersService.create(createUserInput);
}
}
// entities/user.entity.ts (GraphQL support)
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { ObjectType, Field, Int } from '@nestjs/graphql';
@Entity()
@ObjectType()
export class User {
@PrimaryGeneratedColumn()
@Field(() => Int)
id: number;
@Column()
@Field()
name: string;
@Column({ unique: true })
@Field()
email: string;
@Column()
password: string; // Not exposed in GraphQL
@Column()
@Field()
createdAt: Date;
}
Testing and Debugging
// users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
const mockUserRepository = () => ({
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
});
type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;
describe('UsersService', () => {
let service: UsersService;
let repository: MockRepository;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useFactory: mockUserRepository,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
repository = module.get<MockRepository>(getRepositoryToken(User));
});
describe('findOne', () => {
it('should successfully retrieve a user', async () => {
const userId = 1;
const expectedUser = {
id: userId,
name: 'Test User',
email: '[email protected]',
createdAt: new Date(),
updatedAt: new Date(),
};
repository.findOne.mockResolvedValue(expectedUser);
const result = await service.findOne(userId);
expect(repository.findOne).toHaveBeenCalledWith({ where: { id: userId } });
expect(result).toEqual(expectedUser);
});
it('should throw exception for non-existent user ID', async () => {
const userId = 999;
repository.findOne.mockResolvedValue(null);
await expect(service.findOne(userId)).rejects.toThrow('User not found');
});
});
describe('create', () => {
it('should create a new user', async () => {
const createUserDto = {
name: 'New User',
email: '[email protected]',
password: 'password123',
};
const savedUser = {
id: 1,
...createUserDto,
password: 'hashed_password',
createdAt: new Date(),
updatedAt: new Date(),
};
repository.findOne.mockResolvedValue(null); // No existing user
repository.create.mockReturnValue(savedUser);
repository.save.mockResolvedValue(savedUser);
const result = await service.create(createUserDto);
expect(repository.save).toHaveBeenCalled();
expect(result).toEqual(expect.objectContaining({
id: 1,
name: createUserDto.name,
email: createUserDto.email,
}));
});
});
});
// users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
const mockUsersService = () => ({
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
});
describe('UsersController', () => {
let controller: UsersController;
let service: jest.Mocked<UsersService>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useFactory: mockUsersService,
},
],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get(UsersService);
});
describe('findAll', () => {
it('should return a list of all users', async () => {
const expectedUsers = [
{ id: 1, name: 'User 1', email: '[email protected]' },
{ id: 2, name: 'User 2', email: '[email protected]' },
];
service.findAll.mockResolvedValue(expectedUsers);
const result = await controller.findAll(1, 10);
expect(service.findAll).toHaveBeenCalledWith(1, 10);
expect(result).toEqual(expectedUsers);
});
});
});