A progressive Node.js framework for building efficient and scalable server-side applications.
- This repository contains the entire NestJs learning journey that is created By (
Stephen Grider
). - Build full featured backend APIs incredibly quickly with Nest.
- TypeORM, and Typescript. Includes testing and deployment!.
- Deploy a feature-complete app to production
- Write integration and unit tests to ensure your code is working
- Use an API client to manually test your app
- Make your code more reusable and testable with dependency injection
- Use decorators to dramatically simplify your code
- Build authentication and permissions systems from scratch
- Tie different types of data together with TypeORM relationships
- Use Guards to prevent unauthorized users from gaining access to sensitive data
- Model your app's data using TypeORM entities
You can find it on Udemy here: NestJS - Stephen Grider - Udemy
^ | Install Package | Description |
---|---|---|
1 | @nestjs/[email protected] | Contains vast majority of functions, classes, etc, that we need from NEST |
2 | @nestjs/[email protected] | |
3 | @nestjs/[email protected] | Lets Nest use ExpressJs for handling http requests |
4 | [email protected] | Helps make decorators work. |
5 | [email protected] | We write nest apps with typescript |
{
"compilerOptions": {
"module": "CommonJS",
"target": "es2017",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
$ npx ts-node-dev src/main.ts
$ nest new <name> [options]
// <name> The name of the new project
- Generate a module
(nest g mo)
to keep code organized and establish clear boundaries (grouping related components) - Generate a controller
(nest g co)
to define CRUD routes (or queries/mutations for GraphQL applications) - Generate a service
(nest g s)
to implement & isolate business logic - Generate an entity class/interface to represent the resource data shape
- Generate Data Transfer Objects (or inputs for GraphQL applications) to define how the data will be sent over the network
-
Tell nest to use global validation, HOW?
import { ValidationPipe } from "@nestjs/common"; app.useGlobalPipes(new ValidationPipe());
in 'main.ts' file we import ValidationPipe from common. then we use it inside bootstrap function:
-
Create A DTO Class inside dtos folder for required validation: Call it for example: "CreateMessageDto" and use some npm packages class-validator and class-transformer to make use of decorators to validate your fields.
# dtos/create-message-dto.ts import { IsString } from 'class-validator'; export class CreateMessageDto { @IsString() content: string; }
-
Inside your controller go to the post method and make the body to be a dto object .
import { CreateMessageDto } from './dtos/create-message-dto'; @Post() createMessage(@Body() body: CreateMessageDto) { console.log(body); }
-
Read this
Never Do this
BAD: Message Service creates its own copy of MessagesRepository
export class MessagesService {
messagesRepo: MessagesRepository;
constructor() {
// DO NOT DO THAT in real apps: use Dependency Injection
this.messagesRepo = new MessagesRepository();
}
}
Lets Solve this with Dependency Injection.
Instead, Do this
Better: MessagesService receives its dependency.
export class MessagesService {
messagesRepo: MessagesRepository;
constructor(repo: MessagesRepository) {
this.messagesRepo = repo;
}
}
Read this First: Implementing a Generic Repository Pattern Using NestJS
MessagesService receives its dependency, and it doesn't specifically required 'Message Repository' you can change your datasource in a flexible way.
interface Repository {
findOne(id: string);
findAll();
create(content: string);
}
export class MessagesService {
messagesRepo: Repository;
constructor(repo: Repository) {
this.messagesRepo = repo;
}
}
Stephen Grider says:
last thing I want to touch on is the fact that as you start to look at this code, you might really start to say, what is the benefit? What have we really gained here? What is the big benefit to all the stuff we've done? Who cares about using dependency injection? Well, to be honest with you, sometimes I kind of agree with you not going to lie. Sometimes in invest making use of dependency injection feels like we are just kind of jumping through extra hoops without really gaining a whole lot.
So if you feel that way, totally fine. But I can't tell you without a doubt, is that testing your application when you're making use of dependency injection and its entire inversion of control technique, testing your app is going to be far easier, a lot easier. So eventually when we start writing tests around our application, we're going to see that testing individual classes inside of our app is going to be very simple and straightforward compared to if we were not making use of inversion of control and dependency injection. So just keep in mind that is really the payout. The payout of all this stuff is once we start writing tests. So that's kind of got a corollary to it, I'm assuming that you are interested in testing and you want to do testing.
$ nest new di
$ nest genrate module $(name)
$ nest genrate service $(name)
$ nest genrate controller computer
//1) create user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
@Column()
password: string;
}
//2) update users.module.ts
+ import { TypeOrmModule } from '@nestjs/typeorm';
+ import { User } from './user.entity';
@Module({
+ imports: [TypeOrmModule.forFeature([User])],
})
export class UsersModule {}
//3) update parent: app.module.ts
+ import { User } from './users/user.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'db.sqlite',
+ entities: [User],
synchronize: true,
}),
UsersModule,
ReportsModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
``
More about: Repository Api
//1) Update users.service.ts
+ import { Repository } from 'typeorm';
+ import { InjectRepository } from '@nestjs/typeorm';
+ import { User } from './user.entity';
@Injectable()
export class UsersService {
+ constructor(@InjectRepository(User) private repo: Repository<User>) {}
//this @InjectRepository(User) for purpose of dependency injection with generics.
create(email: string, password: string) {
const user = this.repo.create({ email, password });
return this.repo.save(user);
}
}
//2) Update users.controller.ts
+ import { UsersService } from './users.service';
@Controller('auth')
export class UsersController {
+ constructor(private usersService: UsersService) {}
@Post('/signup')
createUser(@Body() body: CreateUserDto) {
+ this.usersService.create(body.email, body.password);
}
}
No hooks works in save api.
- In 'user.entity.ts'
+ import { Exclude } from 'class-transformer';
// Add Exclude Decorator to the field that it must be hidden.
+ @Exclude()
@Column()
password: string;
- In 'user.controller.ts'
import { UseInterceptors, ClassSerializerInterceptor } from "@nestjs/common";
// Add this decorator in your route.
+ @UseInterceptors(ClassSerializerInterceptor)
@Get('/:id')
async findUser(@Param('id') id: string) {
const user = await this.usersService.findOne(+id);
if (!user) {
throw new NotFoundException('User not found');
}
return user;
}
What's the difference between Interceptor vs Middleware vs Filter in Nest.js?
create new interceptor 'serialize.interceptor.ts'
import {
UseInterceptors,
NestInterceptor,
ExecutionContext,
CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
import { plainToClass } from "class-transformer";
export class SerializeInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
handler: CallHandler<any>
): Observable<any> | Promise<Observable<any>> {
return handler.handle().pipe(
map((data: any) => {
return plainToInstance(this.dto, data, {
excludeExtraneousValues: true,
});
})
);
}
}
create new decorator to make use of our interceptor
export function Serialize(dto: any) {
return UseInterceptors(new SerializeInterceptor(dto));
}
Use it in our route or, or in our controller
@Serialize(UserDto)
@Get('/:id')
findUser(){}
to avoid this @Serialize('Hello World')
Use interface for class definition
interface ClassConstructor {
new (...args: any[]): {};
}
function Serialize(dto: ClassConstructor){...}
Create a new service class and inject it to the module 'auth.service.ts'
Then it will be able to use UserService as a di.
// auth.service.ts
import { Injectable } from "@nestjs/common";
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
}
// users.module.ts
@Module({
+ providers: [UsersService, AuthService],
})
export class UsersModule {}
Signup functionality in the auth service class.
export class AuthService {
async signup(email: string, password: string) {
// Check if user email in use
const users = await this.usersService.find(email);
if (users.length) {
throw new BadRequestException("This email is already use.");
}
// Hash the user password
// First: Generate the Salt
const salt = randomBytes(8).toString("hex");
// Hash the salt and the password together using scrypt function as a buffer.
const hash = (await scrypt(password, salt, 32)) as Buffer;
// Join the hashed result and the salt together.
// you will split it in the sign in function to get the salt and hash.
// then with the login password you will hash it again with the given salt.
// then compare them if they equal = Authenticated user.
const result = `${salt}.${hash.toString("hex")}`;
// Create a new user and save it
const user = await this.usersService.create(email, result);
// return created user.
return user;
}
async signin(email: string, password: string) {
// Check if user email exist.
const [user] = await this.usersService.find(email);
if (!user) {
throw new BadRequestException("This email not exist.");
}
// Check if password is correct.
const [salt, storedHash] = user.password.split(".");
const hash = (await scrypt(password, salt, 32)) as Buffer;
if (storedHash !== hash.toString("hex")) {
throw new BadRequestException("Wrong Password.");
}
return user;
}
}
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
export const CurrentUser = createParamDecorator(
(data: any, context: ExecutionContext) => {
return "Current User Custom Decorator";
}
);
- Session object to get the current userId, easy with context object.
- UserService class to get access to repo, hard because the dependency injection system.
Solution for this problem: Create an Interceptor to hold those things, and make it inside di container and then decorator can use it.
1- Create Interceptor to add the current user to the request.
import {
NestInterceptor,
ExecutionContext,
CallHandler,
Injectable,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { UsersService } from "../users.service";
@Injectable()
export class CurrentUserInterceptor implements NestInterceptor {
constructor(private userService: UsersService) {}
async intercept(
context: ExecutionContext,
handler: CallHandler
): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const { userId } = request.session;
if (userId) {
const user = await this.userService.findOne(+userId);
request.currentUser = user;
}
return handler.handle();
}
}
2- Inject the interceptor to the providers of 'users.module.ts'
//users.module.ts
@Module({
...,
providers: [..., CurrentUserInterceptor],
});
3- Use the interceptor in our controller.
//users.controller.ts
@UseInterceptors(CurrentUserInterceptor)
export class UsersController {}
// in users.module.ts
+ import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
...,
providers: [
...,
{
provide: APP_INTERCEPTOR,
useClass: CurrentUserInterceptor,
},
],
})
export class UsersModule {}
- Globally for all controllers.
- Controller scope.
- Single Handler Scope.
1- Make your guard dir, and create 'auth.guard.ts'
import {
CanActivate,
ExecutionContext,
UnauthorizedException,
} from "@nestjs/common";
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const { userId } = request.session;
if (!userId) {
throw new UnauthorizedException(
"Your are not authorized for this resource."
);
}
return userId;
}
}
2- In our controller:
import { UseGuards } from "@nestjs/common";
import { AuthGuard } from "../guards/auth.guard";
// @UseGuards(AuthGuard) // apply guard for controller
export class UsersController {
@Get("/whoami")
@UseGuards(AuthGuard) // apply guard for single handler
whoAmI(@CurrentUser() user: User) {
return user;
}
}
- A Copy of UsersService to use [find(), create()].
- The UsersService depends on 'UsersRepo', So it should have copy of it.
- The UsersRepo depends on 'SQLITE' and its configuration.
We're going to use the dependency injection
system to avoid having to create all these different dependencies.
import { Test } from "@nestjs/testing";
import { AuthService } from "./auth.service";
import { User } from "./user.entity";
import { UsersService } from "./users.service";
it("can create an instance of auth service", async () => {
//Create a fake copy of users service.
const fakeUsersService: Partial<UsersService> = {
find: () => Promise.resolve([]),
create: (email: string, password: string) =>
Promise.resolve({ id: 1, email, password } as User),
};
const module = await Test.createTestingModule({
providers: [
AuthService,
{
provide: UsersService,
useValue: fakeUsersService,
},
],
}).compile();
const service = module.get(AuthService);
expect(service).toBeDefined();
});
To solve the problem of : 'Test module cannot use session or pipe because these are created in main.ts file not in the app module... We can do this: '
// Remove this block of code in bootstrap function >> main.ts
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
})
);
import { ValidationPipe } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
providers: [
...,
{
provide: APP_PIPE,
useValue: new ValidationPipe({ whitelist: true }),
},
],
// Remove this block of code in bootstrap function >> main.ts
app.use(
cookieSession({
keys: ["secret-key"],
})
);
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(cookieSession({ keys: ["secret-key"] })).forRoutes("*");
}
}
TypeOrmModule.forRoot({
type: 'sqlite',
+ database: process.env.NODE_ENV === 'test' ?
'test.sqlite' : 'db.sqlite',
...
}),
$ npm install @nestjs/config
Stephen Grider Says:
Let me tell you what you and I are going to do. We're going to kind of forget the rules set up by the Dotenv docs people, we're not going to very strictly follow their rules. Instead, we're going to take the Nest recommendation. We are going to have more than one different file. We're going to have one specifically to be used during development of our application and one during testing of our application. So these different files are going to provide some different database names for this database property right here. We'll have one file. It says use a database name of DB_SQLITE. And then the other file will say use a database name of something like Test.SQLite or something like that. So, again, we are not strictly following the recommendations of env dOCS because I don't know, I'm just not really a big fan of that library myself, as you can tell, with all this invariant configuration stuff.
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
// Global your config variables.
// choose which file compiler use based on NODE_ENV variable.
ConfigModule.forRoot({
isGlobal: true,
envFilePath: `.env.${process.env.NODE_ENV}`,
}),
// Add ConfigService to dependency container of typeorm module.
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => {
return {
type: 'sqlite',
database: config.get<string>('DB_NAME'),
entities: [User, Report],
synchronize: true,
};
},
}),
// Comment out this part.
// TypeOrmModule.forRoot({
// type: 'sqlite',
// database: process.env.NODE_ENV === 'test' ? 'test.sqlite' : 'db.sqlite',
// entities: [User, Report],
// synchronize: true,
// }),
...,
],
...
})
It’s a relationship where a record in one entity (table) is associated with exactly one record in another entity (table).
- Country - capital city: Each country has exactly one capital city. Each capital city is the capital of exactly one country.
This kind of relationship means that one row in a table (usually called the parent table) can have a relationship with many rows in another table (usually called child table).
- One customer may make several purchases, but each purchase is made by a single customer.
By definition, a many-to-many relationship is where more than one record in a table is related to more than one record in another table.
- A typical example of a many-to many relationship is one between students and classes. A student can register for many classes, and a class can include many students.
// in user.entity.ts
// Doesn't make a change in our Database
class User {
@OneToMany(() => Report, (report) => report.user)
reports: Report[];
}
// in report.entity.ts
// Create user_id column in reports table in Database.
class Report {
@ManyToOne(() => User, (user) => user.reports)
user: User;
}
// in reports.service.ts
export class ReportsService {
create(reportDto: CreateReportDto, user: User) {
const report = this.repo.create(reportDto);
report.user = user; // assign user to the report
return this.repo.save(report);
}
}
// in reports.controller.ts
@Controller("reports")
export class ReportsController {
@Post()
@UseGuards(AuthGuard)
@Serialize(ReportDto)
// to hide some information of user.: Serialize it --> hide password property
createReport(@Body() body: CreateReportDto, @CurrentUser() user: User) {
return this.reportsService.create(body, user);
}
}
ManyToOne: Create user_id column in reports table in Database.
- I cannot understand the syntax of callback passed to the TypeORM Relations decorator
- Circular Dependency TS Issue
- Circular Type References in TypeScript
Very similar to our AuthGuard that we build here: AuthGuard Implementation
import { CanActivate, ExecutionContext } from "@nestjs/common";
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
if (!request.currentUser) {
return false;
}
return request.currentUser.admin;
}
}
1- Create 'admin.guard.ts' in src/guards directory.
import { CanActivate, ExecutionContext } from "@nestjs/common";
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
if (!request.currentUser || !request.currentUser.admin) {
throw new UnauthorizedException(
"Your are Unauthorized for this resource."
);
}
return true;
}
}
2- In our reports controller:
import { UseGuards } from "@nestjs/common";
import { AdminGuard } from "../guards/admin.guard";
export class ReportsController {
@Patch("/:id")
@UseGuards(AdminGuard)
approveReport(@Param("id") id: string, @Body() body: ApproveReportDto) {
return this.reportsService.changeApproval(+id, body.approved);
}
}
The issue is : The Guards execute before the interceptors, and we get our currentUser from the interceptor.
Solution: We need to take our CurrentUserInterceptor and turn it into a middleware
instead to be executed before the guards.
1- Create 'current-user.middleware.ts' file inside users/middlewares directory
import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response, NextFunction } from "express";
import { UsersService } from "../users.service";
@Injectable()
export class CurrentUserMiddleware implements NestMiddleware {
constructor(private usersService: UsersService) {}
async use(req: Request, res: Response, next: NextFunction) {
const { userId } = req.session || {};
if (userId) {
const user = await this.usersService.findOne(+userId);
// @ts-ignore
// this is cause an error: we will solve it later...
req.currentUser = user;
}
next();
}
}
2- In users module class:
// remove all this lines::
import { CurrentUserInterceptor } from './interceptors/current-user.interceptor';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
// remove this dependency
{
provide: APP_INTERCEPTOR,
useClass: CurrentUserInterceptor,
}
});
import { Module, MiddlewareConsumer } from "@nestjs/common";
import { CurrentUserMiddleware } from "./middlewares/current-user.middleware";
export class UsersModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(CurrentUserMiddleware).forRoutes("*");
}
}
Property 'currentUser' does not exist on type 'Request'.
Solution: by applying this code, now we add a new property to Request Interface of Express namespace.
declare global {
namespace Express {
interface Request {
currentUser?: User;
}
}
}
// get-estimate.dto.ts
// Create this dto to validate the query string and transform it to the needed datatype.
import {
IsLatitude,
IsLongitude,
IsNumber,
IsString,
Max,
Min,
} from "class-validator";
import { Transform } from "class-transformer";
export class GetEstimateDto {
@IsString()
make: string;
@IsString()
model: string;
@Transform(({ value }) => parseInt(value))
@IsNumber()
@Min(1930)
@Max(2050)
year: number;
@Transform(({ value }) => parseFloat(value))
@IsLongitude()
lng: number;
@Transform(({ value }) => parseFloat(value))
@IsLatitude()
lat: number;
@Transform(({ value }) => parseInt(value))
@IsNumber()
@Min(0)
@Max(1_000_000)
mileage: number;
}
// reports.service.ts
// filter reports and get average of the top 3 results.
createEstimate({ make, model, lng, lat, year, mileage }: GetEstimateDto) {
return this.repo
.createQueryBuilder()
.select('AVG(price)', 'price')
.where('approved IS TRUE')
.andWhere('make = :make', { make })
.andWhere('model = :model', { model })
.andWhere('lng - :lng BETWEEN -5 AND 5', { lng })
.andWhere('lat - :lat BETWEEN -5 AND 5', { lat })
.andWhere('year - :year BETWEEN -3 AND 3', { year })
.orderBy('ABS(mileage - :mileage)', 'DESC')
.setParameters({ mileage })
.limit(3)
.getRawOne();
}
// reports.controller.ts
@Get()
getEstimate(@Query() query: GetEstimateDto) {
return this.reportsService.createEstimate(query);
}
export class AppModule {
constructor(private configService: ConfigService) {}
configure(consumer: MiddlewareConsumer) {
consumer
.apply(cookieSession({ keys: [this.configService.get("COOKIE_KEY")] }))
.forRoutes("*");
}
}
Synchronize makes the entity sync
with the database every time you run your application. Hence, whenever you add columns to an entity, create a new table by creating a new entity, or remove columns from an existing table by modifying an entity (made a migrations)
it will automatically update the database once the server is started.
Even Though synchronization is a good option to synchronize your entity with the database, it is unsafe for production databases. Therefore migrations can be an alternative solution for safer migrations in production databases.
Well, I want you to imagine a scenario where maybe you are making some changes to your code and maybe by mistake you delete some property off your user entity.
Let's imagine that you then saved your entire project and deployed it off to a production environment. When your application starts up on that production server, it's going to connect to your production database type forums, then going to see your updated user entity. And it's going to notice that it no longer has a password property.
So typeorm is going to automatically go into your production database and delete that entire password column. When that delete occurs, you have now lost data inside of your application. That is obviously really, really bad and definitely not something we would ever want to do.
So the downside to using this synchronize flag of true is that you can very easily accidentally lose data inside of your production database the next time you deploy your app. So even though it can be really handy to use during development, because we can very quickly make changes to our database just by changing our different entity files, I highly recommend that you do not make use of synchronize true as soon as you start deploying your application to a production environment.
It is crucial to think about the structure of our database carefully. Even if we do that, the requirements that our application has to meet change. Because of the above, we rarely can avoid having to modify the structure of our database. When doing that, we need to be careful not to lose any existing data.
With database migrations, we can define a set of controlled changes that aim to modify the structure of the data. They can include adding or removing tables, changing columns, or changing the data types, for example. While we could manually run SQL queries that make the necessary adjustments, this is not the optimal approach. Instead, we want our migrations to be easy to repeat across different application environments.
Also, we need to acknowledge that modifying the structure of the database is a delicate process where things can go wrong and damage the existing data. Fortunately, writing database migrations includes committing them to the repository. Therefore, they can undergo a rigorous review before merging to the master branch. In this article, we go through the idea of migrations and learn how to perform them with TypeORM.
Read TypeORM Config Docs
1- Create in root directory a new folder 'db' and create in it a new file 'data-source.ts'
import { DataSource, DataSourceOptions } from "typeorm";
export const dataSourceOptions: DataSourceOptions = {
type: "sqlite",
database: "test.sqlite",
entities: ["dist/**/*.entity.js"],
migrations: ["dist/db/migrations/*.js"],
};
const dataSource = new DataSource(dataSourceOptions);
export default dataSource;
2- in our 'app.module.ts' lets use our dataSourceOptions.
import { dataSourceOptions } from '../db/data-source';
// Remove all stuff related to TypeOrmModule and put this new import
@Module({
imports: [
...,
TypeOrmModule.forRoot(dataSourceOptions),
],
});
3- To CREATE Migration: Put these scripts in 'package.json'
"scripts": {
"typeorm": "npm run build && npx typeorm -d dist/db/data-source.js",
"migration:generate": "npm run typeorm -- migration:generate",
"migration:run": "npm run typeorm -- migration:run",
"migration:revert": "npm run typeorm -- migration:revert"
}
4- To Create a new Migration run this command:
$ npm run migration:generate -- db/migrations/newMigration