Nestjs Course Manual Haider Malik
Nestjs Course Manual Haider Malik
js Course Manual
Chapter 1 Getting Started with Nest.js
What is Nest.js
What Problem Nestjs Solves
Why Nest.js was created
Why should you use Nest.js
Benefits of using Nest.js
What type of Application can you build
Who is using Nest.js in production
Creating a Nestjs Project
Steps to create Nest.js project
Create a new project using Nest cli
Start the Project
Project Directory structure
Chapter 2 Creating REST APIs
What is a module in Nest.js
What is a Nest.js Controller
What is Service
Creating a Service
Inject the Service into the Controller
Test the GET Songs Route
Test the POST Songs Route
Chapter 3 Middlewares / Exception Filters and Pipes
What is Middleware in Nest.js
Execute any code within middleware
Modify the request (req) object
End the response cycle
Call the next middleware in the stack.
Logger Middleware
Creating Logger Middleware
Apply middleware in the AppModule.
Test the middleware
Handling Exceptions
Handling Exception with Try/Catch
Pipes
Validate request parameters with the class validator
Global Scope pipes
Create CreateSongDTO
Apply CreateSongDTO as Body decorator
Test the Application
Chapter 4 Dependency Injection
Custom Providers
Various techniques exist for utilizing providers in NestJS:
Standard Providers
Value Providers
Providers
Class Providers: useClass
Non-Service Provider
Injection Scopes
Chapter 5 Connecting Nest.js application with TypeORM and Postgres
Install Dependencies
Import TypeORM Module to App Module
Test the DB Connection
Create an Entity
Update CreateSongDTO
Register the Entity in AppModule
Test the Application
Create and Fetch Records from DB
Repository Pattern
Create Record
Find All Records
Find Record by ID
Delete Record
Update Record
Test the Application
Pagination
Install Pagination Package
Create new Paginate method
Refactor findAll method in controller
Test the Application
Apply Sorting/Orderby
Chapter 6 Relations
One to One Relation
Create Artist Entity
Create User Entity
Add One to One Relation
Register User and Artist in AppModule
Many to Many Relation
Add Many to Many Relation in Artist
Add Many to Many Relation in Song
Refactor CreateSongDTO
Refactor UpdateSongDTO
Register the Artist Entity in SongsModule
Refactor the Create Song Method
Test the Application
One to Many Relation
Create a Playlist Entity and add Relations
Add Many to One Relation in Song Entity
Add One to Many Relation in User
Create PlayList Module
Create PlayList Service
Create a Data Transfer Object DTO for the Playlist
Create PlayListsController
Register the PlayList Module in AppModule
Test the Application
Chapter 7 Authentication
User Signup
Install Dependencies
Create User and Auth Module
Creating AuthController
Creating UsersService
Create CreateUserDTO
Refactor the User Entity
Test the Application
User Login
What is JSON Web Token Authentication
Install Passport
Create Login Route and Handler
Create LoginDTO
Create findOne method inside UsersService
Test the Application
Authenticate User
Install Dependencies
Import JWT Module in AuthModule
Refactor the AuthService
Create a new file to save Constant Values
Refactor Auth Module
Create JWT Strategy Service
Register the JWTStrategy in AuthModule
Create JWT Guard
Protect Private Route in AppController
Test the Application
What is Role-based Authentication
Create an Artists Module
Add ArtistsModule into AppModule
Create ArtistsService
Refactor login method in AuthService
Create a PayloadType
Create a new JwtArtistGuard
Refactor the validate method in JwtStrategy
Apply JwtArtistGuard on creating songs endpoint
Test the Application
What is Two Factor Authentication
Install Dependencies
Update User Entity
Add new Enable2FA type
Add a new method in AuthService to enable two-factor auth
Use base32 secret key in QR code App
Refactor UsersService
Create Endpoint in AuthController to enable 2FA
Test the Enable Authentication Endpoint
Implement Disable 2-Factor Authentication
Verify One-time password/token
If user enabled the 2FA refactor the login method
What is an API Key Authentication
When do you need API Keys
Where can we use API Keys
Complete Flow of API Key Authentication
Steps
Step 1: Generate API Keys
Step 2: Create and Store the API key
Step 3: Create an API Key strategy
Step 4: Register API key strategy in Auth Module
Step 5: Validate the User by API key
Step 6: Apply API key Authentication on protected Route
Create a launch.json file in the .vscode folder
Start the Application using the Debug script
Chapter 8 Migrations
Why do we need Migrations
Step 1: Move TypeORM config into a separate file
Step 2: Refactor TypeORM config in AppModule
Step 3: Migration scripts in package.json file
Step 4: Add a new column in any Entity
What is Data Seeding
1. Install Dependencies
2. Create a seed-data.ts file in the db/seeds directory
3. Create a new seed module
4.Create a SeedService
5. Run the Seeds
Chapter 9 Configuration
Step 1: Install Dependencies
Step 2: Import ConfigModule in AppModule
Step 3: Creating Custom Env Files
Step 4: Make ConfigModule to global
Step 5: Creating CustomConfiguration.ts file
Step 6: Load Configuration File
Step 7: Test the env variable
Step 8: Use ConfigService in main.ts file
Step 9: Add JWT secret in Configuration
Step 10: Setup DB Configuration
Application Configurations
Step 1: Add Node_ENV variable in .env files
Step 2: Create .env.validation.ts
Step 3: Add validate method in ConfigModule
Step 1: Create webpack-hmr.config.js
Step 2: Refactor bootstrap function in main.ts
Step 3: Refactor start:dev script in package.json
Chapter 9 API Documentation with Swagger
What is Swagger?
Step 1: Install Dependencies
Step 2: Configure SwaggerModule in bootstrap function
Step 1: Add auth Tags in the Auth Controller
Step 2: Add Api Operation and Response for the Signup flow
Step 3: Refactor the TypeORM config in data-source.ts
Show User Schema
Step 1: Add @ApiProperty in the User Entity
Step 2: Register the Swagger plugin in nest-cli.json
User Authentication
Step 1: Add API Operation for Login
Step 2: Enable Bearer Auth
Step 3: Update Secret Key in JWTStrategy
Step 3: Apply ApiBearerAuth on the protected Route
Chapter 10 MongoDB Database
Install MongoDB using Docker Compose
Step 1: Create a new Project
Step 2: Install Dependencies
Step 3: Setup MongoDB using Docker Compose
Step 4: Connect MongoDB Database using GUI Tool
Connect with MongoDB
Step 1: Create Mongoose Module in the AppModule
Create Schema
Step 1: Create a Schema
Save Record in MongoDB
Step 1: Create Songs Module
Step 2: Create Songs Controller
Step 3: Create Songs Service
Step 4: Add a create method in SongsController
Step 5: Create CreateSongDTO
Step 6: Add a create method in SongsService
Step 7: Register the Song Model in SongsModule
Step 8: Test the Application
Find and Delete Record
Step 1: Create a new find method in SongService
Step 2: Create a Route in SongController
Step 3: Create a findById method in SongService
Step 4: Create findOne Route in the Controller
Step 5: Create a delete song method in SongService
Step 6: Create a Route for deleting a song
Populate
Step 1: Create an album schema
Step 2: Add a relation in the song schema
Step 3: Create Album Module, Controller and Service
Refactor the CreateSongDTO
Chapter 11 Deploy Nest.js Application
Configure Development and Production Environment
Step 1: Create New Account at the Railway
Step 2: Create a new Project
Step 3: add NODE_ENV in configuration file
Step 4: Refactor the envFile path in AppModule
Step 4: Add NODE_ENV for development and production script
Step 5: Test the Application
Pushing your source code
Step 1: Create a GitHub Repository
Deploy Nest.js Project
Step 1: Connect your GitHub Repo to Railway Project
Step 2: Create a Database
Step 3: Copy the Database Configurations
Step 1: Connect your GitHub Repo to Railway Project
Step 2: Create a Database
Step 3: Copy the Database Configurations
Step 4: Set the Environment Variables for your Project
Step 5: Test the Application
Install DotEnv
Step 1: Run the Migration
Step 2: Set the Environment to Production using the export command
Step 3: Run the migration again
Step 4: Install the dotenv package
Step 5: Run the migration again to test it
Fixing Env Bugs
Step 1: Create a .env file
Step 2: Generation the Migrations
Step 3: Run the migration command
Step 4: Push the code to Repo
Chapter 12 Testing
Getting started with Jest
What is Jest
Step 1: Create Package.json
Step 2: Create a sum.js file
Step 3: Write a test case
Auto-Mocking
What is Auto Mocking
What are Mock Functions
Step 1: Basic Mock Function
Step 2: Basic Mock Function with Arguments
Step 3: Create a Mock Function
Step 4: Create Mock Function with a Promise
SpyOn Functionality
Step 1: SpyOn existing Object
Step 2: Spy on the class Method
Step 3: restore All mocks usuing beforeEach hook
Unit Test Controller
Unit Testing
Step 1: Run the Test
Step 2: Creating a Mock SongService
Step 3: Test Controller functions
Unit Test Service
Step 1: Run the test for SongService
Step 2: Create the Mock Repository
Step 3: Test SongService
End To End Testing
What is E2E Testing?
Step 1: Add E2E script
Step 2: Add TypeORM Module
Step 3: Clear SongRepository
Step 4: Test Get Songs endpoints
Step 5: Test GET Song Endpoint
Step 6: Test PUT Song Endpoint
Step 7: Test Create Song Endpoint
Step 8: Test Delete Song Endpoint
Chapter 13 WebSocket and SocketIo Integration
Using Speedy Web Compiler
Step 1: Uninstall the @nestjs/cli
Step 2: Install @nestjs/cli
Step 3: Create a new Project
Step 4: Make speedy web compiler
Step 5: Install the SWC packages
Step 6: Run the Application
Create Web Socket Server
What are WebSockets?
Step 1: Install Dependencies
Step 2: Create Events Module and Gateway
Step 3: Create a new Event
Send Message from Frontend Client
Step 1: Consuming Event from the Frontend
Step 2: Return data from the message event using Observable
Summary
Chapter 14 Build GraphQL APIs
GraphQL Server Setup
Step 1: Install Dependencies
Step 2: Import GraphQL Module
Step 3: Create a Schema File
Step 4: Generate Typings
Step 4: Create Script for the Typings
Define Queries and Mutations
Step 1: Define a query for a single song
Step 2: Define Mutations
Step 3: Generate Typings
Resolve Queries and Mutations
Step 1: Refactor type UpdateSongInput to input
Step 2: Create Song Resolver
Step 3 Register the Song Resolver in the Song Module
Step 4: Resolve Song By a given Id
Resolver with Song Mutation
Step 1: Add CreateSongInput
Step 2: Resolver with createSong mutation
Step 3: Resolver with Update Song Mutation
Step 4: Resolver with Delete Song Mutation
Error Handling
Chapter 15 Authenticate GraphQL APIs
Define Schema for Authentication
Resolve Auth Queries and Mutations
Step 1: Refactoring SignupInput
Step 2: Creating the Auth Resolver
Step 2: Resolver with Signup Mutation and Login Query
Apply Authentication using AuthGuard
Step 1: Define a Profile Query
Step 2: Create a Resolver for the Profile object
Step 3: Create a GraphQLAuth authentication guard
Step 4: Test the Application
Subscriptions in GraphQL
Step 1: Installing graphql subscriptions
Step 2: Create Subscription
Step 3: Resolve the Subscription
Step 4: Publish the event
Chapter 16 Testing GraphQL APIs
Unit Test Resolver
Step 1: Run the Test
Step 2: Mock the SongService
Step 3: Test getSongs from the Song Resolver
Step 4: Unit Test createSong from the Song Resolver
Step 5: Unit Test updateSong from the Song Resolver
Step 6: Test deleteSong from the Song Resolver
End to End Test
Step 1: Adding End to End watch script to package.json file
Step 2: Create a new E2E Testing File
Step 3: Setup a E2E Testing File
Step 4: Test the Songs Query
Step 5: Test A Single Song Query
Step 6: Test The Creation of a aong mutation
Step 7: Testing The Update Song Mutation
Step 8: Test The Delete Song Mutation
Chapter 17 GraphQL Advanced Concepts
Server Side Caching
Step 1: Adding End to End watch script to package.json file
Step 2: Create a new E2E Testing File
Step 3: Setup a E2E Testing File
Step 4: Test the Songs Query
Step 5: Test A Single Song Query
Step 6: Test The Creation of a aong mutation
Step 7: Testing The Update Song Mutation
Step 8: Test The Delete Song Mutation
Optimize Query Performance using DataLoader
What is DataLoader?
Run the Application without DataLoader
Step 1: Create UsersLoader in the users folder
Step 2: Register Loader in Context
Step 3: Load data from a loader in PostResolver
Step 4: Run the Application
Fetching Data from External API
What is RESTDatasource in Apollo
Step 1: Create Schema file
Step 2: Implement RESTDatasource in TodoService
Step 3: Use DataSource in TodosResolver
Step 4: Add Datasource in context
Chapter 18 Prisma Integration with Nest.js
Setup Prisma
What is Prisma?
Step 1: Setup Prisma
Step 2: Create Database
Step 3: Install Prisma
Step 4: Initialize Prisma Project
Models and Migrations
What is Prisma Model
Create Prisma Model
Run Migrations
Creating Prisma Service
Step 1: Setup Prisma Client
Step 2: Create PrismaService
Create, FindOne and Find
Step 1: Create a Resource
Step 2: Update SongService
Step 3: Update SongController
Step 4: Register PrismaService
Step 5: Test the Application
Update and Delete
Step 1: Implement the Update method
Step 2: Update Controller
Step 3: Delete Song
One to Many Relation
Step 1: Define Relation
Step 2: Generate Artist Resource
Step 3: Create Artist
Step 4: Add Artist's relation during Song Creation
One to One Relation
Step 1: Add Relation
Step 2: Run Prisma Migration
Step 3: Generate User Resource
Step 4: Save User with Profile
Step 5: Find a user with Profile
Many to Many Relation
Use Case
Step 1: Add Many to Many Relation
Step 2: Run Migrations
Step 3: Create Posts Resource
Step 4: Create Post
Step 5: Create a Post by making a relationship with Existing Categories
Step 6: Relation Queries
Nested Queries
What is Transaction
When to use nested writes
Use Case
Step 2: Run migrations
Step 3: Generate Application resource
Step 4: Inject Prisma dependency
Step 5: Create the Application
Batch Bulk Operations
Interactive Transactions
Use Case:
Process / Flow
Step 1: Create Model
Step 2: Run Migrations and generate resource
Step 3: Create two new accounts
Step 4: Create route for transfer account
Step 5: Implement Account Transfer Logic
Test Transfer Process
Chapter 19 Additional Nestjs Concepts
File Upload
Step 1: Install Multer Types
Step 2 Create upload route handler
Step 3: Upload file with Validations
Custom Decorator
What are Custom Dectorators?
Step 1: Create Custom Decorator
Step 2: Create User Entity
Step 3: Apply User Decorator
Running CRON Task
Step 1: Install Packages
Step 2: Register ScheduleModule in AppModule
Step 3: Create TaskService and define CRON Job
Cookies
What are Cookies?
Step 1: Install Packages
Step 2: Register Cookie Parser
Step 3: Set Cookies
Step 4: Test
Queues
What are Queues in Nest.js?
Step 1: Install Dependencies
Step 2: Creating Audio Module
Step 3: Setup Redis with docker-compose
Step 4: Register BullModule
Step 5: Register BullModule Queue
Step 6: Creating audio convertor endpoint
Step 7: Implement Audio Processor
Event Emitter
What is an Event Emitter?
Why do we need it?
Use Case
Step 1: Install Dependencies
Step 2: Register EventEmitter Module
Step 3: Create AudioConvertedListener
Step 4: Create AudioConvertedEvent Type
Step 5: Emit the Event in AudioProcessor
Step 6: Run the Application
Streaming
What is Streaming
Practical Use Cases of Streaming in Nest.js:
Step 1: Create a FileController
Step 2: Download the file
Session
What is a session?
Practical Uses of Session
Step 1: Install Dependencies
Step 2 Register Middleware
Step 3: Create Login Route Handler
Step 4: Profile Route
What is Nest.js
Nest.js is a Node.js framework designed for crafting efficient, reliable, and scalable
server-side applications. Built on top of Express and TypeScript, it adopts Node.js and
Angular design patterns to provide a cohesive development experience. The
framework’s architecture embraces decorators and dependency injection, patterns
borrowed from Angular, to facilitate better code organization and reusability.
What Problem Nestjs Solves
While unopinionated frameworks like Express offer multiple ways to structure your
code, the freedom to choose can lead to decision paralysis, wasting time that could
be better-spent building features. Nest.js sidesteps this issue by providing a modular
approach right out of the box, allowing for flexible code organization while still
enforcing a structured layout, aligning with its principle of modularity.
Essentially, the issue Nest aims to solve is creating a robust architecture for backend
applications. By providing a well-defined structure and integrating established design
patterns, it answers a pressing question many developers have about organizing
architecture for enterprise or large server-side applications.
The creator of Nest.js drew inspiration from Angular’s design architecture to build a
front-end application. Transferring this idea to the server side allowed for the broad
utilization of a familiar architecture that incorporates Nest. js-specific elements like
modules, dependency injection, providers, and custom decorators. This strategy
embodies the Nest.js principle of modularity and extensibility, making it easier for
developers who are already familiar with Angular to adopt Nest.js.
Why should you use Nest.js
If you aim to build server-side APIs utilizing TypeScript, Nest.js is the framework of
choice. Nest.js employs strong typing and decorators, elements intrinsic to
TypeScript, thus aligning closely with TypeScript principles. It also supports
Dependency Injection out-of-the-box, encouraging a modular, scalable architecture.
Nest.js is particularly accessible for developers familiar with Angular, making the
learning curve more manageable. Both frameworks share common programming
paradigms and syntax, including the use of decorators and modules, enhancing code
reusability and maintainability.
Nest.js enables you to construct various backend services, from RESTful APIs and
GraphQL endpoints to MVC applications and WebSockets. The framework’s flexibility
accommodates multiple communication patterns, allowing for an extensive range of
backend solutions.
Nest.js provides native integrations with a host of external tools like Mongoose for
MongoDB, TypeORM for various databases including Postgres, and many more. This
extensibility allows developers to incorporate a multitude of functionalities without
boilerplate code, making the framework more versatile.
● Utilize Angular-style syntax for the backend, a feature unique to Nest.js, that
promotes consistency and reusability across the tech stack. This syntax allows
developers familiar with Angular to seamlessly transition into backend
development.
● Leverage the detailed documentation with examples that Nest.js provides. This
not only accelerates the learning curve but also aligns with Nest.js’s principle
of being developer-friendly and easy to pick up.
● Benefit from the framework’s focus on good architecture and fast
development. Nest.js follows the modular architecture pattern, allowing for
better separation of concerns and easier testing, which in turn speeds up the
development process.
● You can build backend REST and GraphQL APIs using Nest.js, benefiting from
its modular architecture that promotes code reusability and maintainability.
The framework’s built-in support for these query languages facilitates quick
API development.
● You can build microservices with Nest.js, taking advantage of its inherent
support for multiple message transport layers. This aligns with the
microservices architecture principle, which advocates for loosely coupled,
independently deployable components.
● You can construct a backend for a streaming application using Nest.js, as the
framework supports asynchronous data handling through Observables. Nest.js
makes it easier to work with real-time data streams, thanks to its integration
with libraries like RxJS.
● You can build the backend of a real-time application in Nest.js, utilizing its
WebSocket support for real-time two-way communication. This is essential for
applications requiring instant data update and interaction, adhering to the
principles of real-time system design.
Before creating the Nestjs project. Make sure you installed Node. js on your machine.
You have successfully installed the nest cli. Now it is time to create a new project
using the Nest cli command nest new [name of the project]
The project with the n-fundamentals name will be created. You can choose any
project name here. When the project is created successfully you can start the project
by using npm run start:dev
Nest.js designates the src folder as the location where you’ll place your application’s
source code. This arrangement adheres to Nest.js’s modular architecture, promoting
better organization and separation of concerns within your application.
Within the src folder, the main.ts file serves as the entry point of your Nest.js
application. This file is responsible for bootstrapping the application, utilizing Nest.js’s
core function NestFactory to create an instance of your Nest application.
The main.ts file serves as the entry point for the application and employs the core
function NestFactory to instantiate a Nest application. In Nest.js, NestFactory is pivotal
for bootstrapping the application, setting up the dependency injection system, and
initializing modules. This follows the Nest.js principle of modular development and
centralized configuration.
Your application will contain a Root Module, which is specific to the Nest.js
framework. The Root Module serves as the entry point and is responsible for
instantiating controllers, providers, and other core elements of the application. In
Nest.js, this architecture follows the “Module Isolation” principle, ensuring that the
application is organized into distinct functional or feature-based modules.
@Module({
imports: [SongsModule],
controllers: [AppController],
providers: [AppService],
})
We are going to build the backend of the Spotify application. We can divide our
application into Modules in this way
Divide your application’s use cases into feature modules, such as the Artist Module,
Songs Module, and Auth Module. In Nest.js, modules are a fundamental organizational
unit that follow the Modularization principle, enabling better code reusability and
separation of concerns. This approach streamlines development, as modules
encapsulate related functionalities and can be developed or maintained
independently.
Each Module will have its own providers, services, and controllers.
Now it’s time to create a module. Create a songs module using nest cli
A songs module will be created in your application. It will also add the SongsModule
entry in AppModule
What is a Nest.js Controller
Controllers in Nest.js are responsible for handling incoming requests and managing
the logic to send responses back to the client. They act as the “C” in the MVC
(Model-View-Controller) pattern that Nest.js leverages for application architecture.
In the context of building the backend of a Spotify-like application, let’s say you want
to fetch all songs by the artist Martin Garrix. The responsibility to handle this type of
request lies primarily with controllers, specific to this use case— the
SongsController.
Your browser will initiate a request to fetch all songs. In your Nest.js application,
you’ll handle this through the SongsController and its findAll method, which is
specifically designed to interact with underlying services to retrieve data and send it
back to the client.
GET http://localhost:3000/songs
GET http://localhost:3000/songs/1
POST http://localhost:3000/songs
PUT http://localhost:3000/songs/1
DELETE http://localhost:3000/songs/1
You can create a controller very easily. We are going to use the Nest cli to create a
controller
Have the SongsController inside the songs directory. Nestjs will also added the
entry for the SongsController in SongsModule.
@Controller("songs")
export class SongsController {
@Post()
create() {
return "create a new song endpoint";
}
@Get()
findAll() {
return "find all songs endpoint";
}
@Get(":id")
findOne() {
return "fetch song on the based on id";
}
@Put(":id")
update() {
return "update song on the based on id";
}
@Delete(":id")
delete() {
return "delete a song on the based on id";
}
}
We’re sending simple messages to indicate that the route has been created. In Nest.js,
you can specify dynamic parameters id1 in your route by using a colon followed by
the parameter name, like @Get(':id'). This follows the Nest.js principle of utilizing
decorators to handle common HTTP tasks, streamlining the codebase and making it
more readable.
What is Service
Services in Nest.js are providers, meaning you can inject them into modules and
classes through dependency injection. In Nest.js, a service is not just a construct but
a first-class citizen, managed by the framework’s built-in Inversion of Control (IoC)
container. Unlike in Express.js, where middleware or simple JavaScript functions
often serve the same purpose but without formal dependency management, Nest.js
services offer a structured way to write business logic, making the application more
maintainable and testable.
A service in Nest.js is responsible for fetching data from the database and saving data
back to the database, functioning as a liaison between the controller and the
database. This concept aligns with Nest.js’s adherence to the Single Responsibility
Principle, separating the business logic from the controller layer, a stark contrast to
Express.js where the roles are often less clearly delineated.
In this lesson, we’ll focus on creating the song service, a specific component tailored
to manage song-related data. Creating specialized services for different aspects of
your application promotes better maintainability and is a cornerstone of Nest.js’s
modular architecture.
Creating a Service
@Injectable()
Injecting Service
@Controller('songs')
We can inject the service into a constructor function. Now the SongsController is
dependent on SongsService
This command will create the SongsService inside the songs folder. One more thing, It
will register the SongsService into SongsModule in the provider array
@Module({
controllers: [SongsController],
providers: [SongsService],
})
Let’s create an array of songs and this service will interact with the songs array. We
will interact with the database next section.
@Injectable()
export class SongsService {
// local DB
// local array
private readonly songs = [];
create(song) {
// Save the song in the database
this.songs.push(song);
return this.songs;
}
findAll() {
// fetch the songs from the db
return this.songs;
}
}
Now a songs array has been created and two methods: create and findAll.
● Use findAll to send the array of songs in the response. In Nest.js, this is a
common use case for a GET request handler within a controller, and it’s more
declarative compared to handling routes in Express.js, which often requires
middleware for such functionality.
● Utilize create to add new songs to the array. This aligns with Nest.js’s design
philosophy of structuring code around well-defined modules and services, as
opposed to Express’s more flexible but less structured approach.
Inject the Service into the Controller
Use the SongsService within the SongsController. In Nest.js, this demonstrates the
Dependency Injection (DI) system, which is a fundamental principle of the framework
for decoupling components. Unlike in Express.js, where middleware and route
handling functions often mingle with business logic, Nest.js encourages a more
structured, modular approach that aligns well with SOLID principles.
Run the application and send a request to http://localhost:3000/songs. This API request
will return all the songs; if no items are present in the songs array, expect a []
response with a 200 status code. Unlike in Express, where you would manually define
the response code and body, Nest.js leverages decorators and dependency injection
to streamline API response management.
When you send this request POST http://localhost:3000/songs from rest-client. It will
create a new song in the songs array.
When you send the get songs request again you will get the songs array in the
response.
The middleware will have access to req, res, and the next function, allowing
customization of the request object. This is similar to Express, but Nest.js provides a
more robust and scalable architecture for building complex applications.
In traditional Express.js, this is often done directly within the middleware function. In
Nest.js, however, you can lean more on Dependency Injection (DI) and modularity to
make these changes in a more organized fashion.
Just like in Express, middleware in Nest.js can terminate the request-response cycle.
However, Nest.js middleware leverages async/await and decorators, offering a more
modern approach and cleaner syntax for handling such operations.
Both in Nest.js and Express, middleware can pass control to the next middleware
function in the stack using the next() function. However, Nest.js brings type safety
and DI into the picture, making it easier to build robust and maintainable applications.
Logger Middleware
Send the request to the server via a browser. In the Nest.js application, execute the
logger middleware before running the request handler. This architecture follows
Nest.js’s modular approach, wherein middleware-like logging functions can be
organized and re-used across different modules more effectively than in a framework
like Express, which lacks such a built-in modular system.
Logger systems are essential for tracking activity, diagnosing issues, and
understanding the behavior of an application. Unlike traditional setups where logging
might be an afterthought, Nest.js allows the integration of sophisticated logging
mechanisms due to its modular and extensible nature. This is in contrast to less
opinionated frameworks like Express, where logging is often implemented via
external middleware without any standard structure.
We are going to use nest cli to generate the LoggerMiddleware. Please create the
common folder inside the src directory and also create a middleware folder inside the
common directory. This could be the directory structure src/common/middleware/
@Injectable()
next();
configure(consumer: MiddlewareConsumer) {
// consumer.apply(LoggerMiddleware).forRoutes('songs'); // option no 1
// consumer
// .apply(LoggerMiddleware)
// .forRoutes({ path: 'songs', method: RequestMethod.POST }); //option no 2
consumer.apply(LoggerMiddleware).forRoutes(SongsController); //option no 3
Choose from the three provided options to apply the middleware. In the last option,
implement the LoggerMiddleware for the SongsController routes.
When sending a request to any songs API route, ensure it displays the current date.
Handling Exceptions
If an error occurs in the code, handling it becomes crucial. NestJS offers built-in
HTTP exception handling that streamlines the process of sending informative,
well-structured responses to the client, a feature that sets it apart from frameworks like
Express, which require additional middleware for similar functionality.
return this.songs;
A fake error message has been sent to simulate an issue while fetching data from the
database. Sending a request to fetch all songs from http://localhost:3000/songs
will result in an error message accompanied by a 500 status code.
// SongsContoller.ts
@Get()
findAll() {
try {
return this.songsService.findAll();
catch (e) {
'server error',
HttpStatus.INTERNAL_SERVER_ERROR,{ cause: e },
);
Pipes
● Transform Param using ParseInt is a feature in NestJS that allows for easy type
conversion. In contrast to frameworks like Express, which lack built-in
parameter transformation, NestJS’s use of pipes offers a more automated and
native approach to type coercion, adhering to best practices for robust type
checking, which a principal engineer would highly value.
● There are two primary use cases for pipes: transforming the value and
validating the input parameters. While Express requires middleware or
additional libraries like express-validator to achieve similar functionality,
NestJS pipes integrate seamlessly into the framework’s ecosystem, offering a
more elegant, maintainable solution for both value transformation and input
validation—aligned with the architectural best practices that a principal
engineer would implement.
// SongsContoller.ts
@Get(':id')
findOne(
@Param(
'id',
new ParseIntPipe({
errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE
}),
),
id: number,
) {
● Dynamic parameters can be captured using the @Param decorator, where the
argument name needs to be specified. In contrast to Express, where request
parameters are extracted using req.params, NestJS provides a more
declarative and type-safe way to do so, adhering to best practices by enforcing
a stricter type system.
● The id parameter is of type string by default. Utilizing ParseIntPipe will
automatically convert this string value to a number. Unlike in Express, which
would require manual type conversion, NestJS’s use of pipes allows for
automatic type transformation, making the code more robust and maintainable,
a practice any principal engineer would appreciate.
● Sending a request to http://localhost:3000/songs/1 will result in logging
the type of id as a number. This showcases NestJS’s ability to utilize pipes for
transformation tasks, an area where it holds an edge over frameworks like
Express, which necessitate separate middleware for such operations.
● The error status code can also be provided to ParseIntPipe. Should a string
value be provided, an error will be generated. This approach lends itself to
better error handling in NestJS compared to the more manual error-checking
methods required in Express.
● Sending a request to http://localhost:3000/songs/abc will produce an
error message stating “not acceptable.” In frameworks like Express, validation
logic for handling such errors would generally need to be written explicitly,
whereas NestJS allows for more configurable and built-in validation
mechanisms. This feature aligns with best practices for maintainability and
scalability.
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0"
· The next action involves binding the ValidationPipe from the @nestjs/common
package. This feature provides an advantage over frameworks like Express, where
validation often necessitates additional libraries or custom middleware. In NestJS,
pipes offer multiple scopes for validation: parameter-scoped, method-scoped,
controller-scoped, or global-scoped, lending greater flexibility and modularity to
the application.
· Opting for a global scope requires registration in the main.ts file. This is a best
practice for ensuring application-wide consistency in validation, as it minimizes
the chances of missing validation logic in any part of the application. Unlike
Express, which would require a global middleware function for similar
functionality, NestJS makes it straightforward to set global validation rules.
src/main.ts
// ...
app.useGlobalPipes(new ValidationPipe());
Create CreateSongDTO
A DTO (Data Transfer Object) serves as a blueprint for how data will be sent over the
network. While both TypeScript interfaces and simple classes can define the DTO
schema, classes are recommended in this context. In contrast to Express, where DTO
and validation often require additional libraries or middleware, NestJS provides a
more integrated approach through its decorator-based validation techniques. As a best
practice, storing DTOs in a dedicated directory ensures that the application adheres to
the principle of separation of concerns, facilitating easier maintenance and future
scaling.
import {
IsArray,
IsDate,
IsDateString,
IsInt,
IsMilitaryTime,
IsNotEmpty,
IsString,
} from "class-validator";
@IsString()
@IsNotEmpty()
readonly title;
@IsNotEmpty()
@IsArray()
readonly artists;
@IsDateString()
@IsNotEmpty()
@IsMilitaryTime()
@IsNotEmpty()
@Post()
return results;
}
To initiate the application, execute the command npm run start:dev and proceed to
send the ‘create song’ request. Unlike in Express, where nodemon or a similar
package would be separately installed for hot-reloading, NestJS includes this feature
by default with the start:dev script, providing a more out-of-the-box development
experience. As a best practice, separating the ‘create song’ logic into a dedicated
service method ensures cleaner, more maintainable code.
POST http://localhost:3001/songs
Content-Type: application/json
"artists": ["Siagla"],
"duration" :"02:34"
Custom Providers
1. Standard Providers: These are classes that get instantiated by the NestJS
dependency injection system. Unlike in Express, where dependency injection
must often be implemented manually or through third-party libraries, NestJS
provides this feature natively. As a best practice, leverage standard providers
for services that require instantiation.
2. Value Providers: These are hard-coded values or configurations injected into
other classes. This feature can replace the need for environment variables or
configuration files, which in frameworks like Express, would typically be
managed by separate packages. Using value providers for constants and
configuration settings contributes to code maintainability.
3. Non-Class based Provider Tokens: These are custom tokens that can be used to
inject values or services. Unlike in frameworks like Django, which relies more
on function-based views and doesn’t have a direct equivalent, NestJS allows
greater flexibility in dependency injection. Use custom tokens judiciously to
avoid overly complex dependency graphs.
4. Class Providers: useClass: These allow for the substitution of one class
provider with another, providing polymorphism. This is distinct from Express,
where such abstraction might require more manual factory patterns. Employing
useClass is beneficial for achieving code reusability and flexibility.
5. Factory Providers: useFactory: These allow for conditional instantiation of a
class, something not natively supported in frameworks like Flask. Use factory
providers when the provision of a class or value is contingent upon runtime
conditions.
6. Non-service Providers: These are providers that aren’t necessarily tied to a
service and may provide utility functions, for example. This granularity is
generally not available in basic Express setups and would usually require
additional modules or utilities. Incorporating non-service providers can aid in
keeping the codebase DRY (Don’t Repeat Yourself).
Exploration of all six techniques will take place, each elucidated through an example.
We are going to play around with all these 6 techniques. I will explain each technique
with the help of an example
Standard Providers
@Module({
controllers: [SongsController],
providers: [SongsService],
})
This is the standard provider technique, you have used in our application. You can
also convert the above syntax into this syntax
@Module({
controllers: [SongsController],
providers: [{
provide: SongsService,
useClass: SongsService,
},
];
})
Value Provider
const mockSongsService = {
findAll() {
return [{
id: 1,
title: "Lasting lover"
}];
},
};
@Module({
controllers: [SongsController],
providers: [
SongsService,
provide: SongsService,
useValue: mockSongsService,
},
],
})
When you send GET request to localhost:3000/songs it will run the findAll()
method from mockSongsService instead of origianl SongsService
// src/common/constants/connection.ts
CONNECTION_STRING: "CONNECTION_STRING",
DB: "MYSQL",
DBNAME: "TEST",
};
CONNECTION_STRING: string;
DB: string;
DBNAME: string;
};
Providers
// src/common/constants/connection.ts
CONNECTION_STRING: "CONNECTION_STRING",
DB: "MYSQL",
DBNAME: "TEST",
};
CONNECTION_STRING: string;
DB: string;
DBNAME: string;
};
import {
connection
} from 'src/common/constants/connection';
@Module({
controllers: [SongsController],
providers: [SongsService]
providers: [
SongsService,
provide: 'CONNECTION',
useValue: connection,
},
],
})
The connection object can now be injected into any controller or service within the
SongsModule. This contrasts with frameworks like Express, where dependency
injection isn’t native and often requires third-party libraries like ‘Awilix’ for similar
functionality. As a best practice, isolating database connections in dedicated provider
files ensures more modular and maintainable code, thereby facilitating easier unit
testing and separation of concerns.
@Injectable()
constructor(
@Inject("CONNECTION")
connection: Connection
) {
}
I have injected this connection object into SongsService by using the @Inject
decorator with the token name CONNECTION
@Module({
providers: [
AppService,
provide: DevConfigService,
useClass: DevConfigService,
},
],
})
A DevConfigService class has been created and registered as a provider. With this
setup, AppModule is capable of injecting this provider as a dependency into any class
for utilization. In contrast to Express, which typically relies on third-party libraries
like dotenv for configuration management, NestJS offers a more integrated way to
handle configurations through custom service providers. As a best practice,
encapsulating configuration logic within a dedicated service class enables easier
testing and maintenance, adhering to the principle of Separation of Concerns.
// src/common/providers/DevConfigService.ts
import {
Injectable
} from "@nestjs/common";
@Injectable()
DBHOST = "localhost";
getDBHOST() {
return this.DBHOST;
@Injectable()
getHello(): string {
The DevConfigService has been injected into AppService, and the getDBHOST
method from DevConfigService is invoked. Unlike in Express, where dependency
injection is not natively supported and might require third-party libraries, NestJS
simplifies this with built-in Dependency Injection, leading to more modular and
testable code. As a best practice, environment-specific configurations like database
host details should be abstracted away into separate configuration services, ensuring
that the code adheres to the Twelve-Factor App methodology.
Non-Service Provider
A provider in NestJS has the flexibility to supply any value, making it a versatile
component for dependency injection. The useFactory syntax facilitates the dynamic
creation of providers, a feature that sets it apart from Express, which often relies on
external libraries like awilix or manual constructor injection for similar functionality.
As a best practice, isolating complex business logic within factory providers can lead
to more modular and testable code.
const devConfig = {
port: 3000
};
const proConfig = {
port: 400
};
@Module({
providers: [{
provide: 'CONFIG',
useFactory: () => {
},
},
],
})
Two objects, devConfig and proConfig, have been created. Dynamic value
assignment is possible through the useFactory function. Unlike Express, where
environment-specific configurations often require separate JSON or JS files, NestJS
allows for more streamlined, inline dynamic configuration. As a best practice,
isolating configuration logic in dedicated modules or services fosters maintainability
and scalability.
@Injectable()
constructor(
@Inject("CONFIG")
private config: {
port: string
}
) {
console.log(config);
1. DEFAULT
● In this scope, a single instance of the provider is shared across the entire
application, a feature similar to Express’s singleton services but made explicit
in NestJS. When the application requests this provider a second time, NestJS
retrieves it from an internal cache, a mechanism that enhances application
performance. As a best practice, caching providers to minimize computational
overhead is advisable.
2. REQUEST
● Contrary to the DEFAULT scope, a new instance of the provider is created for
each incoming request. While this provides better isolation, it’s a different
model from Express’s middleware-based architecture where request-level
isolation usually involves function scope. This scope is ideal for scenarios
where each request may need a provider with state that shouldn’t affect other
requests.
3. TRANSIENT
● Transient providers are unique in that they are not shared across different
consumers. When a transient provider is injected, the consumer receives a
new, dedicated instance, unlike in Express where this level of injection
granularity is not native and often must be manually managed. Employing
transient providers is good practice when state isolation between consumers is
crucial.
@Injectable({
scope: Scope.TRANSIENT
})
@Controller({
path: "songs",
scope: Scope.REQUEST,
})
Install Dependencies
"@nestjs/typeorm": "^9.0.1",
"pg": "^8.10.0",
"typeorm": "^0.3.15"
"devDependencies": {
"@types/node": "^18.11.18"
AppModule is our root module, we have to configure the TypeORM module here
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'your_username',
password: 'your_db_password',
database: 'n-test',
entities: [],
synchronize: true,
}),
]
● Importing the TypeORM module into AppModule requires calling the forRoot
method. NestJS streamlines manual configuration for integrating with
databases by providing built-in methods like forRoot that make it easier to set
up a database connection. As a best practice, separating database connection
logic into a dedicated configuration file is advised for easier management and
scalability.
● The setting synchronize: true is risky for production as it can result in data loss.
As a good practice, maintaining separate configurations for development and
production environments ensures database integrity.
● After creating a new entity, it must be added to a specified array. NestJS
promotes organization by having entities declared in a central location. A best
practice is to maintain an index or a barrel file that exports all entities, thus
making it easier to manage them.
● The forRoot() method accepts all configuration properties exposed by the
DataSource constructor in the TypeORM package. As a good practice,
encapsulating these settings in environment variables offers both security and
ease of management.
● Providing a username and DB password is essential, and no database with the
name n-test will exist by default. In NestJS, tools like PG-ADMIN allow for a
graphical interface to interact with the Postgres Database. A good practice is to
use a secure vault or environment variables for storing sensitive information
like usernames and passwords.
console.log(dataSource.driver.database);
Utilizing dependency injection for the datasource class promotes a modular and
easily testable codebase, aligning with industry best practices. Additionally, logging
crucial steps, such as successful database connections, aids in debugging and
monitoring, also considered a best practice
Create an Entity
@Entity("songs")
id: number;
@Column()
title: string;
artists: string[];
releasedDate: Date;
duration: Date;
lyrics: string;
In the class Entity: @Entity('songs') indicates that songs is the name of the
database table. @PrimaryGeneratedColumn() serves to automatically increment the
primary key, a feature that may be specified either here or directly within the database.
@Column() defines a column, allowing specification of various data types such as
date, varchar, and time. This column definition offers precise mapping, crucial for
efficient data querying and comprehensive schema definition.
artists: string[];
An array of artists exists, serving as a data structure to hold multiple artist entities. To
create this array field in a Postgres database, metadata is added to the @Column
decorator in the NestJS application. As a best practice, strongly typing this array—for
instance, as string[]—can improve code maintainability and reduce potential
runtime errors. In addition, leveraging Postgres’ native array data type within the
@Column decorator provides optimizations for array operations and queries.
releasedDate: Date;
The date type is specified for the release date, ensuring that no time-related properties
are added to the releasedDate field. In NestJS, using class-validator’s @IsDate()
decorator on the releasedDate property would further enforce type safety and
validation, adhering to best practices for robust, production-ready code
duration: Date;
The duration field is configured with the date type, which might imply that it should
hold date-related data. However, despite this type declaration, the field will
exclusively contain time information because the specific type has been set to time.
As a best practice in NestJS, explicitly naming the database column and its type using
the @Column decorator can avoid ambiguity and make the code more maintainable.
Additionally, using type guards or DTOs (Data Transfer Objects) with class-validator
can ensure that the data in this field adheres to the intended time format, enhancing
data integrity.
lyrics: string;
To accommodate a long string or text, the text type in Postgres is appropriate. This
stands in contrast to shorter text, for which the varchar type would be suitable.
Update CreateSongDTO
@IsString()
@IsOptional()
A new field, lyrics, has been added to store the lyrics in the database. This field was
not previously included in the CreateSongDTO file.
To integrate the Song Entity with the application, include it in the AppModule by
updating the TypeORM module. Specifically, add the Song Entity to the entities
array within the forRoot method. NestJS allows seamless integration of entities via
its modular structure. As a best practice, organizing the entities in a dedicated
configuration file can enhance modularity and ease of management.
entities: [Song],
Upon running the application, the songs table appears in the n-test database. These
fields materialize automatically, an outcome facilitated by NestJS’s integration with
TypeORM, which handles database migrations and schema synchronization. A best
practice involves leveraging NestJS’s built-in Dependency Injection system for all
database interactions, ensuring a modular and testable codebase.
TypeORM supports the repository design pattern, so each entity has its own
repository. In NestJS, this adherence to the repository pattern facilitates cleaner, more
modular code by separating the database logic from business logic, aligning with
software engineering best practices.
These repositories can be obtained from the database data source. In a NestJS
application, you usually inject these repositories into your services or controllers via
Dependency Injection, enabling direct interaction with the database through methods
like find, save, or delete.
@Module({
imports: [TypeOrmModule.forFeature([Song])],
controllers: [SongsController],
providers: [SongsService],
})
To use the SongRepository we need to import the TypeORM module into the Songs
module. This module uses the forFeature() method to define which repositories are
registered in the current scope. With that in place, we can inject the SongRepository
into the SongService using the @InjectRepository() decorator:
constructor(
@InjectRepository(Song)
) {}
SongRepository provides CRUD methods to create, delete, update, and fetch records
from the Songs table. In a NestJS application, this specialized repository is
responsible for handling all operations related to the Songs entity, abstracting the
database interactions and thus adhering to the repository best practice.
Create Record
Now, you should implement the create song method. This time, there is no need to add
a record in the local db array. Save the new song in the database by using the
songRepository.save() method
song.title = songDTO.title;
song.artists = songDTO.artists;
song.duration = songDTO.duration;
song.lyrics = songDTO.lyrics;
song.releasedDate = songDTO.releasedDate;
// method
}
Instantiate a new song instance from the Song Entity. Set the fields of the songs table
using the DTO object, and finally, return a promise containing the song object from
the create method.
In the context of NestJS and TypeORM, methods like save(), find(), or delete() often
return promises. By defining a return type like Promise<Song[]> in your service or
controller methods, you make it explicit that the function is asynchronous and will
return data at a future point in time. This is particularly beneficial for type-checking
and for setting the expectations for the developers who will consume these methods.
songRepository provides the save method to save the record in a database table.
You have to provide the instance of the entity which entity record you want to save.
// songs.controller.ts
@Post()
create(@Body() createSongDTO: CreateSongDto): Promise<Song> {
return this.songsService.create(createSongDTO);
Add the return type for the create method, which is a promise containing the Song
entity.
Find All Records
// songs.service.ts
findAll(): Promise<Song[]> {
return this.songRepository.find();
Use the `find1 method in the repository to fetch all records from the database table.
//songs.controller.ts
findAll(): Promise<Song[]> {
return this.songsService.findAll();
Update the return type for the findAll() method in songs.controller.ts. Use
Promise<Song[]> because songService.findAll will return an array of songs.
Find Record by ID
//songs.service.ts
You can find the record by id by using findOneBy() from the songsRepository.
// songs.controller.ts
@Get(':id')
return this.songsService.findOne(id);
Delete Record
// songs.service.ts
await this.songRepository.delete(id);
songRepository provides the delete method to delete the record based on id.
// songs.controller.ts
@Delete(':id')
return this.songsService.remove(id);
Update Record
//songs.service.ts
Create a new update method in songs.service.ts. The first argument should be the
id, and the second argument should be recordToUpdate. Ensure the type of
recordToUpdate is a DTO object.
// src/songs/dto/update-song.dto.ts
import {
IsArray,
IsDateString,
IsMilitaryTime,
IsNotEmpty,
IsOptional,
IsString,
} from "class-validator";
@IsString()
@IsOptional()
readonly title;
@IsOptional()
@IsArray()
@IsString({
each: true
})
readonly artists;
@IsDateString()
@IsOptional()
@IsMilitaryTime()
@IsOptional()
@IsString()
@IsOptional()
Make all these fields optional, as it depends on the user which record they want to
update. Use the @IsOptional() decorator for each field to indicate this.
// songs.controller.ts
@Put(':id')
update(
PUT http://localhost:3001/songs/3
Content-Type: application/json
"releasedDate": "2022-09-30",
"duration": "02:45",
"lyrics": "Sby, you're my adrenaline. Brought out this other side of me You
don't even know Controlling my whole anatomy, oh Fingers are holding you right at
the edge You're slipping out of my hands Keeping my secrets all up in my head I'm
scared that you won't want me back, oh I dance to every song like it's about ya I
drink 'til I kiss someone who looks like ya I wish that I was honest when I had you
I shoulda told you that I wanted you for me I dance to every song like it's about
ya I drink 'til I kiss someone who looks like ya"
Now you can test the application by executing this command npm run start:dev.
You have to send a request to update the record.
Pagination
● Type Safety: Being designed for TypeORM and Nest.js, it ensures type-safe
queries and pagination object returns, reducing the risk of runtime errors.
● Seamless Integration: It’s optimized for Nest.js and TypeORM, making
integration simple and straightforward without needing to modify existing code
significantly.
● Metadata-based: The package automatically computes pagination metadata like
total items, pages, current page number, etc., saving you the time to calculate
these manually.
● Customizable: It allows for detailed customization like specifying relations to
be loaded, limiting fields, and more, enabling more advanced query operations.
"nestjs-typeorm-paginate": "^4.0.3",
Add this package entry in package.json file and run npm install
Create new Paginate method
import {
paginate,
Pagination,
IPaginationOptions,
} from 'nestjs-typeorm-paginate';
// songs.controller.ts
findAll(
page: number = 1,
return this.songsService.paginate({
page,
limit,
});
Set default values for page and limit, and then call the paginate method from
songService, passing in the page and limit values.
Specify the page and limit value on the based on your situation/usecase
"items": [
"id": 10,
"title": "A",
"releasedDate": "2022-09-29",
"duration": "05:00:00",
},
"id": 5,
"artists": ["Siagla"],
"releasedDate": "2022-09-29",
"duration": "02:34:00",
],
"meta": {
"totalItems": 7,
"itemCount": 2,
"itemsPerPage": 2,
"totalPages": 4,
"currentPage": 2
Apply Sorting/Orderby
To apply sorting or filtering, utilize the QueryBuilder feature. Consult the TypeORM
documentation for guidance on using QueryBuilder.
async paginate(options: IPaginationOptions): Promise<Pagination<Song>> {
queryBuilder.orderBy('c.releasedDate', 'DESC');
To sort records, either manually add the “orderBy” field or retrieve it from the query
parameters, depending on your specific use case. Test the application to view records
sorted by “releasedDate” in descending order.
Chapter 6 Relations
One-to-one is a relational database design pattern where entity A contains only one
instance of entity B, and vice versa, ensuring a bijective mapping. In a Nest.js
application with TypeORM, this might manifest as a user entity having a one-to-one
relation with an artist entity, meaning a user can become an artist, and an artist can
have only a single user profile. This design is governed by the Single Responsibility
Principle, as each entity is responsible for a distinct set of attributes and behaviors,
thereby simplifying database management and application logic.
import {
User
} from "src/users/user.entity";
import {
Entity,
JoinColumn,
OneToOne,
PrimaryGeneratedColumn
} from "typeorm";
@Entity("artists")
@PrimaryGeneratedColumn()
id: number;
Define the Artist entity starting with one field, using the @Entity decorator to map
it to a database table. Extend the entity by adding more columns or fields as needed,
guided by the application’s requirements and domain model. This flexibility allows
you to tailor the entity to fit various use cases.
import {
Column,
Entity,
PrimaryGeneratedColumn
} from "typeorm";
@Entity("users")
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
email: string;
@Column()
password: string;
Create a User entity in the application, including fields for firstName, lastName,
email, and password. Use appropriate data types and decorators to define these
fields.
@Entity("artists")
@JoinColumn()
user: User;
Utilize TypeORM’s @OneToOne decorator to specify the target relation type, which in
this case is User. Include the @JoinColumn to ensure that the Artist entity possesses
the relation ID or foreign key; this will result in the Artist table having userId as a
foreign key.
Register the newly created entities in the TypeORM module to integrate them into
your Nest.js application. This can be done within the AppModule, making it a focal
point for configuring these database entities. Following the Dependency Injection
design pattern, this allows your application to be extensible and maintainable by
centralizing the registration process.
Add the @ManyToMany relation in the Artist entity. The first argument specifies the target entity, and
the second argument represents the inverse side of the relationship.
@Entity("songs")
export class Song {
// @Column('varchar', { array: true })
// artists: string[];
Refactor the Artist's column; storing it as an array of strings is unnecessary. Instead, use the @ManyToMany
annotation to define a relationship with another entity, specifying the target entity as the first
argument and the inverse side as the second argument, found in the songs property within the
Artist entity.
For cascading actions, use the cascade: boolean | ("insert" | "update")[] parameter.
Setting it to true will automatically manage the related object's insertion and updates. Specific
cascade options can also be set as an array.
Here, the @ManyToMany and @JoinTable() annotations are typical of TypeORM, which Nest.js
leverages for database operations. The cascade option is an example of the software engineering
principle of DRY (Don't Repeat Yourself), as it automates CRUD operations for related entities.
Meanwhile, the joined table is a clear manifestation of the database normalization principle,
optimizing the data structure.
Refactor CreateSongDTO
Expect an array of numbers, not strings, from the network request object when creating a new song. In the
API request to create a song, provide the artist IDs. Use these IDs to query all corresponding artists from the
database and establish a relationship with the newly created song.
In this context, using artist IDs instead of strings offers type safety and ensures data integrity, which is a
cornerstone of reliable software design. The architecture of expecting IDs and establishing relations through
them is often part of the RESTful API design pattern, which aims for stateless client-server communication.
Nest.js makes it convenient to handle these types of operations through its integration with TypeORM, a
powerful Object-Relational Mapping (ORM) library.
Refactor UpdateSongDTO
@Module({
imports: [TypeOrmModule.forFeature([Song, Artist])],
controllers: [SongsController],
providers: [SongsService],
})
@Injectable()
export class SongsService {
constructor(
@InjectRepository(Song)
private songRepository: Repository<Song>,
@InjectRepository(Artist)
private artistRepository: Repository<Artist>,
) {}
Find the artists by retrieving the ids from the DTO object and set these to the song entity. Save
the changes in the songs repository; this will automatically establish a relation in the joined table
songs_artists.
POST http://localhost:3001/songs
Content-Type: application/json
{
"title": "You for me",
"artists": [1,2],
"releasedDate" : "2022-08-29",
"duration" :"02:34",
"lyrics": "by, you're my adrenaline. Brought out this other side of me You don't
even know Controlling my whole anatomy, oh Fingers are holding you right at the
edge You're slipping out of my hands Keeping my secrets all up in my head I'm
scared that you won't want me back, oh I dance to every song like it's about ya I
drink 'til I kiss someone who looks like ya I wish that I was honest when I had
you I shoulda told you that I wanted you for me I dance to every song like it's
about ya I drink 'til I kiss someone who looks like ya"
}
From a software engineering standpoint, this design pattern enforces the Single Responsibility
Principle (SRP) by clearly defining and separating the responsibilities of each entity. It ensures that
each entity only manages the data and relationships that are directly relevant to it, thereby making
the system easier to understand, debug, and maintain.
import {
Column,
Entity,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
} from "typeorm";
@Entity("playlists")
id: number;
@Column()
name: string;
/**
*/
songs: Song[];
/**
*/
user: User;
Add a @OneToMany relationship between the song and the playlist, as well as a @ManyToOne
relationship between the playlist and the user. The playlist table will include userId as a foreign key.
Add Many to One Relation in Song Entity
/**
* Many songs can belong to the playlist for each unique user
*/
playList: Playlist;
This Song entity will have the playlistId as a foreign key in the songs table.
/**
*/
playLists: Playlist[];
@Module({
controllers: [PlayListsController],
providers: [PlayListsService],
})
Treat the PlayListModule as a feature module within your Nest.js application. In the context of
software modularization, a feature module encapsulates specific functionalities, allowing for clean
separation of concerns. By adopting this approach, you're following the Single Responsibility
Principle, making the application easier to understand, develop, and test.
@Injectable()
constructor(
@InjectRepository(Playlist)
@InjectRepository(Song)
@InjectRepository(User)
) {}
playList.name = playListDTO.name;
// songs will be the array of IDs that we are getting from the DTO object
//Set the relation for the songs with the playlist entity
playList.songs = songs;
// A user will be the ID of the user we are getting from the request
//When we implemented the user authentication this id will become the logged
in user id
playList.user = user;
return this.playListRepo.save(playList);
@IsString()
@IsNotEmpty()
readonly name;
@IsNotEmpty()
@IsArray()
readonly songs;
@IsNumber()
@IsNotEmpty()
To create a PlayList, send a request that includes the name, an array of song IDs, and a user ID. Ensure that
these parameters are formatted correctly in the request payload.
Create PlayListsController
@Controller("playlists")
@Post()
create(
@Body()
playlistDTO: CreatePlayListDto
): Promise<Playlist> {
return this.playListService.create(playlistDTO);
}
}
Create an endpoint to facilitate the addition of a new playlist. Enable the end user to send a POST request to
localhost:3000/playlists for this purpose. Ensure that PlaylistRepository,
SongRepository, and UserRepository are available in the PlayListService.
@Module({
imports: [
TypeOrmModule.forRoot({
}),
SongsModule,
PlayListModule,
],
})
Register the Playlist entity in the entities array. This step is essential for Nest.js to recognize the
entity and make it available for database operations. It aligns with the software engineering principle
of modularization, allowing each entity to serve as an isolated module within the larger application
structure.
"user": 1
Chapter 7 Authentication
User Signup
Save the user in the database after account creation in the application. In a Nest.js environment,
you'll often use a service with injected repository to handle this data-persistence layer. This follows
the software engineering principle of separation of concerns, keeping the data storage logic distinct
from the business logic.
Install Dependencies
Save the user password in an encrypted format. Utilize the bcryptjs package for password
encryption within the Nest.js ecosystem. While it's generally advisable to also use a salt for added
security, this project will focus solely on encryption. Install this dependency to proceed.
Encrypting user passwords is crucial for data security and aligns with the software engineering
principle of confidentiality. The choice of bcryptjs is notable for its reliable and secure encryption
algorithm.
"bcryptjs": "^2.4.3",
You can add this entry into dependencies in the package.json file and run npm install. Install
typescript typing for this package
"@types/bcryptjs": "^2.4.2",
@Module({
imports: [
CatsModule,
SongsModule,
PlayListModule,
UsersModule,
AuthModule,
],
})
//auth.module.ts
import { Module } from "@nestjs/common";
import { AuthController } from "./auth.controller";
import { UsersModule } from "src/users/users.module";
import { AuthService } from "./auth.service";
@Module({
imports: [UsersModule],
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}
We are exporting the AuthService from AuthModule which means when we import the AuthModule
into another module you can use the AuthService or inject the authservice into your imported
module. If you don't have an AuthService you can create it using nest-cli
We are also importing the UsersModule here because we need UserService here.
// users.module.ts
import { Module } from "@nestjs/common";
import { UsersService } from "./users.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { User } from "./user.entity";
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
Creating AuthController
import { Body, Controller, Post } from "@nestjs/common";
import { CreateUserDTO } from "src/users/dto/create-user.dto";
import { User } from "src/users/user.entity";
import { UsersService } from "src/users/users.service";
@Controller("auth")
export class AuthController {
constructor(private userService: UsersService) {}
@Post("signup")
signup(
@Body()
userDTO: CreateUserDTO
): Promise<User> {
return this.userService.create(userDTO);
}
}
We have created a new route for signup to handle the signup request. We did not create a
CreateUserDTO and create method inside the UsersService
Creating UsersService
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { User } from "./user.entity";
import * as bcrypt from "bcryptjs";
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User> // 1.
) {}
Create CreateUserDTO
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
@IsString()
@IsNotEmpty()
lastName: string;
@IsEmail()
@IsNotEmpty()
email: string;
@IsString()
@IsNotEmpty()
password: string;
}
@Column()
@Exclude()
password: string;
}
When working with TypeORM, there might be cases where you want to exclude one or multiple
columns (fields) from being selected. I don't want to send the password in the response that is why I
have added Exclude.
POST http://localhost:3000/auth/signup
Content-Type: application/json
{
"firstName": "john",
"lastName": "doe",
"email": "john@gmail.com",
"password": "123456"
}
User Login
User Authentication: The user provides their credentials (e.g., username and password) to the
authentication server. The server verifies the credentials and generates a JWT if they are valid.
JWT Generation: Upon successful authentication, the authentication server creates a JWT
containing three parts: header, payload, and signature.
Header: It typically consists of two parts: the token type, which is JWT, and the hashing algorithm
used to create the signature. Payload: This contains the claims or statements about the user, such
as their username, role, and any additional information. The payload is not encrypted but is
Base64Url encoded. Signature: The signature is created by combining the encoded header,
encoded payload, and a secret key known only to the server. It ensures the integrity of the token and
prevents tampering. JWT Issuance: The server responds to the user's authentication request by
sending the JWT back as a response.
Token Storage: The client (usually a web browser or a mobile app) stores the received JWT
securely. It can be stored in various places, such as local storage, cookies, or session storage,
depending on the application's requirements.
Token Usage: For subsequent requests to protected resources, the client includes the JWT in the
request headers, typically as the "Authorization" header with the "Bearer" scheme, followed by the
JWT.
Token Verification: When the server receives a request with a JWT, it extracts the token from the
header, payload, and signature.
Signature Validation: The server recalculates the signature using the same algorithm and the secret
key. If the recalculated signature matches the signature in the token, it ensures the token's integrity.
Expiration Check: The server checks the expiration time (exp) claim in the payload to ensure the
token has not expired. If it has expired, the server rejects the request. Additional Validations: The
server may perform additional checks based on the application's requirements, such as verifying the
token's audience (aud) or checking for revoked tokens. Access Grant: If the token passes all the
validations, the server grants access to the requested resource or performs the requested action on
behalf of the user.
Token Renewal: If the token has an expiration time, the client can request a new JWT before the
current one expires. This process is typically done using a refresh token or by re-authenticating the
user.
The JWT authentication flow allows the client to include a token with each request, eliminating the
need for server-side session storage. It enables stateless authentication, making it suitable for
distributed systems and APIs.
Install Passport
"@nestjs/passport": "^9.0.3",
"passport": "^0.6.0",
We have to create a new login route in AuthService. We have called the login method from
Authservice. We have not created the login method in AuthService yet, let's create the login method
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { LoginDTO } from "./dto/login.dto";
import { UsersService } from "src/users/users.service";
import * as bcrypt from "bcryptjs";
import { User } from "src/users/user.entity";
@Injectable()
export class AuthService {
constructor(private userService: UsersService) {}
1. We have to find the user based on email. We need to get the email and password from the
request body.
2. We will compare the user password with an encrypted password that we saved in the last
video
3. If the password matches then delete the user password and send the user back in the
response. It means the user has logged in successfully
4. If the password does not match we have to send the error back in the response
Create LoginDTO
You have to create the LoginDTO file inside the auth/dtos/login.dto.ts
// login.dto.ts
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
POST http://localhost:3001/auth/login
Content-Type: application/json
{
"email": "john@gmail.com",
"password": "123456"
}
We did not send the JSON web token back in the response. In the next lesson, I will teach you how
to send the JSOn web token in the response when the user has successfully logged in
Authenticate User
Find the user from the database based on their email and encrypt their password. If the user logs in
successfully, generate a JSON Web Token (JWT) and include it in the response. This is a key step in
stateless authentication, a software engineering principle that improves scalability and security. In
the previous lesson, the generation and inclusion of the JWT in the response were omitted, which
left the authentication process incomplete.
Install Dependencies
"dependencies" : {
"@nestjs/jwt": "^10.0.3",
"passport-jwt": "^4.0.1",
},
"devDependencies": {
"@types/passport-jwt": "^3.0.8",
}
We have to install these packages to implement complete JSON Web Token authentication and
Authorization
You need to register the JwtModule module in the AuthModule by providing the unique secret key.
We will use this secret key to decode the token or validate the token
/// ...
if (passwordMatched) {
delete user.password;
return user;
// Sends JWT Token back in the response
const payload = { email: user.email, sub: user.id };
return {
accessToken: this.jwtService.sign(payload),
};
}
// auth.constants.ts
export const authConstants = {
secret: "HAD_12X#@",
};
imports: [
UsersModule,
JwtModule.register({
secret: authConstants.secret,
signOptions: { // 1.
expiresIn: '1d',
},
}),
],
@Injectable()
export class JWTStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // 1.
ignoreExpiration: false, // 2.
secretOrKey: authConstants.secret, // 3.
});
}
You have to create new a file inside the auth folder with jwt.strategy.ts
imports: [
PassportModule,
]
providers: [AuthService, JWTStrategy],
You have to register the JWTStrategy as a provider in AuthModule. You also have to register the
PassportModule. It will allow us to use the PassportStrategy class
Create JWT Guard
A Guard is like a middleware in express.js. You can implement role-based authentication using
guards
@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {}
Let's create a provider which is JwtAuthGuard. You have to apply the JWTAuthGuard to protect a
private route
Create a new protected route inside the AppController. When sending a request to access the
profile, the response will include the user ID and email. Apply JwtAuthGuard to any route in any
controller to secure the endpoint.
First, send a login request to obtain the token, and then provide that token in the header.
What is Role-based Authentication
Role-based authentication is a method of access control that regulates user permissions and
privileges within a system based on their assigned roles. In this approach, users are categorized into
roles based on their job responsibilities, functions, or levels of authority within an organization.
Each role is associated with a set of permissions that determine what actions and resources a user
with that role can access. These permissions can include read, write, modify, delete, and other
operations on various system resources such as files, databases, or functionalities.
@Module({
imports: [TypeOrmModule.forFeature([Artist])],
providers: [ArtistsService],
exports: [ArtistsService],
})
export class ArtistsModule {}
@Module({
imports: [
PlayListModule,
UsersModule,
AuthModule,
ArtistsModule,
],
})
Create ArtistsService
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Artist } from "./artist.entity";
@Injectable()
export class ArtistsService {
constructor(
@InjectRepository(Artist)
private artistRepo: Repository<Artist>
) {}
We will use the findArtist method to check if the current logged in user is an artist or not.
1. Refactor the payload method in the login function by changing sub: user.id to userId:
user.id.
2. Find the artist based on the logged-in user.
3. If the user is an artist, save the artist ID in the payload; this artist ID will be used when
decoding the token in the ArtistGuard.
Create a PayloadType
You have to create a payload type inside the auth folder. I have created a separate folder for the
types to store all my types here
//types/payload.type.ts
export interface PayloadType {
email: string;
userId: number;
artistId?: number;
}
// jwt-artist.guard.ts
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { Observable } from "rxjs";
import { PayloadType } from "./types/payload.type";
@Injectable()
export class JwtArtistGuard extends AuthGuard("jwt") {
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
return super.canActivate(context);
}
handleRequest<TUser = PayloadType>(err: any, user: any): TUser {
// 1
if (err || !user) {
//2
throw err || new UnauthorizedException();
}
console.log(user);
if (user.artistId) {
// 3
return user;
}
throw err || new UnauthorizedException();
}
}
1. When you apply the JwtAuthGuard at the controller function it will call the handleRequest
function
2. If there is an error or no user then it will send the unauthorized error
3. Here we are checking the user role, if it is an artist then we have to return the user. This user
will have have email, userId, and artistId property
Now we have protected this endpoint, only artist can access this endpoint and create a new song
{
"email": "john_doe@gmail.com",
"password": "123456"
}
It will give you the token, you have to use that token to create a new song
{
"title": "You for me",
"artists": [1],
"releasedDate" : "2022-08-29",
"duration" :"02:34",
"lyrics": "by, you're my adrenaline. Brought out this other side of me You
don't even know Controlling my whole anatomy, oh Fingers are holding you right at
the edge You're slipping out of my hands Keeping my secrets all up in my head I'm
scared that you won't want me back, oh I dance to every song like it's about ya I
drink 'til I kiss someone who looks like ya I wish that I was honest when I had
you I shoulda told you that I wanted you for me I dance to every song like it's
about ya I drink 'til I kiss someone who looks like ya"
}
Something you know: This factor involves knowledge-based information that the user possesses,
such as a password, PIN, or answers to security questions.
1. Something you have: This factor requires the user to possess a physical device or object,
such as a smartphone, hardware token, or smart card. This device generates or receives a
unique one-time code or a cryptographic key.
2. Something you are: This factor refers to biometric characteristics unique to the individual,
such as fingerprints, facial recognition, iris scans, or voice recognition.
3. The combination of these factors increases the security of authentication because an
attacker would need to compromise multiple elements to gain unauthorized access. Even if
one factor is compromised, the additional factor(s) provide an extra layer of protection.
Here's a simplified example of how two-factor authentication works:
Install Dependencies
Speakeasy is a one-time passcode generator, ideal for use in two-factor authentication, that
supports Google Authenticator and other two-factor devices.
It is well-tested and includes robust support for custom token lengths, authentication windows, hash
algorithms like SHA256 and SHA512, and other features, and includes helpers like a secret key
generator.
Speakeasy implements one-time passcode generators as standardized by the Initiative for Open
Authentication (OATH). The HMAC-Based One-Time Password (HOTP) algorithm defined in RFC
4226 and the Time-Based One-time Password (TOTP) algorithm defined in RFC 6238 are
supported. This project incorporates code from passcode, originally a fork of Speakeasy, and notp.
"speakeasy": "^2.0.0",
You have to add this entry to dependencies in the package.json file and run npm install
"@types/speakeasy": "^2.0.7",
You have to add this entry to devDependencies in the package.json file and run npm install
You have to add two new columns for two-factor authentication. The first column will be used to store
the secret key for each user. The second column will be used to enable or disable the two-factor
authentication.
1. You have to create a new method to find the user on the based on user id
2. If user has already enabled the 2 factor authentication we have to return the secret key.
3. If the user did not enable the 2-factor authentication then we have to generate the secret key.
This speakeasy.generateSecret() will return an object with secret.ascii, secret.hex,
and secret.base32.
4. We are going to use the base32 secret key
5. Finally, we have to update the twoFAsecret key for the specific user
6. You have to return the secret key in the response.
You have received the secret key now you have to create a new app in your QR code application. It
will ask you to add a manual secret key and the name of your app
Refactor UsersService
import { Repository, UpdateResult } from 'typeorm';
//users.service.ts
async updateSecretKey(userId, secret: string): Promise<UpdateResult> {
return this.userRepository.update(
{ id: userId },
{
twoFASecret: secret,
enable2FA: true,
},
);
}
You have to the secret key and enable the 2-factor authentication for a user
//users.service.ts
async findById(id: number): Promise<User> {
return this.userRepository.findOneBy({ id: id });
}
//auth.service.ts
async disable2FA(userId: number): Promise<UpdateResult> {
return this.userService.disable2FA(userId);
}
//users.service.ts
async disable2FA(userId: number): Promise<UpdateResult> {
return this.userRepository.update(
{ id: userId },
{
enable2FA: false,
twoFASecret: null,
},
);
}
GET http://localhost:3000/auth/disable-2fa
Authorization: Bearer
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhaWRlcl9hbGkzQGdtYWlsLmNvbSIs
InN1YiI6NywiaWF0IjoxNjg0NDkyOTk1LCJleHAiOjE2ODQ1NzkzOTV9.vhAHpdyuQHWvsET2sSLvQpr3
3vpk8K089NLiENgh7pM
// auth/dto/validate-token.dto.ts
import { IsNotEmpty, IsString } from "class-validator";
// verify the secret with a token by calling the speakeasy verify method
const verified = speakeasy.totp.verify({
secret: user.twoFASecret,
token: token,
encoding: 'base32',
});
POST http://localhost:3000/auth/validate-2fa
Authorization: Bearer
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InNhbUBnbWFpbC5jb20iLCJzdWIiOjks
ImlhdCI6MTY4NDQ5ODg0MSwiZXhwIjoxNjg0NTg1MjQxfQ.dKgmLSsGctbWR9HKz3ByfS2ZpUGiNR234u
qEgs0pgtQ
Content-Type: application/json
{
"token": "054603"
}
The token I have provided is one-time password/token from the authenticator app. You have to
provide your unique token from your authenticator app
async login(
loginDTO: LoginDTO,
): Promise<{ accessToken: string } | { validate2FA: string; message: string }>
You have to refactor the return type. If the user has enabled the 2FA it will use the second return
type with a custom message
// If user has enabled 2FA and have the secret key then
if (user.enable2FA && user.twoFASecret) { //1.
// sends the validateToken request link
// else otherwise sends the json web token in the response
return { //2.
validate2FA: 'http://localhost:3000/auth/validate-2fa',
message:
'Please send the one-time password/token from your Google Authenticator App',
};
}
return {
accessToken: this.jwtService.sign(payload),
};
1. You have to add a new code here to check user enabled the 2FA.
2. If the user enabled the 2FA then we have to send the link to validate the token from your QR
code app
The purpose of API key authentication is to control and monitor access to an API by assigning and
managing unique keys for individual users or applications.
API keys serve as a form of credential, allowing the API provider to track and control API usage,
enforce rate limits, and monitor and identify unauthorized access attempts.
It enables the API provider to identify and track the usage of different clients, which can be beneficial
for managing access levels, implementing usage quotas, and identifying potential abuse or security
breaches.
It is commonly employed by API providers to ensure secure and controlled access to their services.
By requiring API keys, providers can track usage, enforce restrictions, and have the flexibility to
revoke access for specific keys if necessary.
Steps
1. Step 1: Generate API Keys
2. Step 2: Create and Store API key
3. Step 3: Create an API Key strategy
4. Step 4: Register API key strategy in Auth Module
5. Step 5: Validate the User by API key
6. Step 6: Apply API key Authentication on protected Route
// user.entity.ts
@Column()
apiKey: string;
You have to add the apiKey column in the user entity. Let's create the api key inside the signup
function
// users.service.ts
POST http://localhost:3001/auth/signup
Content-Type: application/json
{
"firstName": "sam",
"lastName": "oven",
"email": "sam@gmail.com",
"password": "123456"
}
{
"firstName": "sam",
"lastName": "oven",
"email": "sam@gmail.com",
"apiKey": "853d94a2-f760-43e3-b384-a9ba94542bf0",
"twoFASecret": null,
"id": 1,
"enable2FA": false
}
@Injectable()
export class ApiKeyStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(apiKey: string) {
const user = await this.authService.validateUserByApiKey(apiKey);
if (!user) {
throw new UnauthorizedException();
} else {
return user;
}
}
}
We have installed the passport-http-bearer package and applied this strategy to validate the API
keys. It means you need to provide the Api key in the authorization header:
When you will apply the AuthGuard @UseGuards(AuthGuard('bearer')) to the protected route. It
will call the validate method from the ApiKeyStrategy
I have created a new function inside the auth service and validated the user by api key
// user.service.ts
async findByApiKey(apiKey: string): Promise<User> {
return this.userRepository.findOneBy({ apiKey });
}
Let's create a new function inside the user.service.ts to fetch the user from the DB based on API
Key
A user can access his/her profile. He has to provide the API key to access the protected route. You
can protect any route by applying the AuthGuard('bearer') to the route
GET http://localhost:3001/auth/profile
Authorization: Bearer 853d94a2-f760-43e3-b384-a9ba94542bf0
You have to provide the API key in the authorization header to execute the request successfully.
{
"version": "0.2.0",
"configurations": [{
"name": "Attach",
"port": 9229,
"request": "attach",
"skipFiles": ["<node_internals>/**"],
"type": "node"
}
]
}
In TypeORM, migrations are a way to manage and apply changes to your database schema over
time. A migration is a file that contains a set of instructions for creating, modifying, or deleting
database tables, columns, constraints, and other schema elements. Migrations help you keep your
database schema in sync with your application's models or entities.
When you develop an application using TypeORM, your database schema evolves as you add new
features, modify existing ones, or fix issues. Migrations provide a structured and controlled approach
to apply these changes to your database without losing existing data.
1. Creating a Migration: When you make changes to your entities or models, you generate a
migration file using TypeORM's CLI command or programmatically using the provided API.
The migration file contains both the "up" and "down" methods. The "up" method specifies
how to apply the changes to the database, while the "down" method defines how to revert
those changes.
2. Applying Migrations: To apply a migration, you execute the migration runner provided by
TypeORM, either through the CLI or programmatically in your code. The runner reads the
migration files and executes the "up" method, which performs the necessary changes to the
database schema.
3. Reverting Migrations: If you encounter issues or need to roll back changes, you can use the
migration runner to revert migrations. The runner executes the "down" method of the
migration file, which undoes the changes made by the corresponding "up" method.
4. Managing Migration History: TypeORM keeps track of the executed migrations in a table
within your database. This table records which migrations have been applied, allowing the
runner to determine which migrations are pending or need to be reverted.
By using migrations, you can version control your database schema, collaborate with other
developers effectively, and easily deploy schema changes across different environments. Migrations
provide a structured and reliable approach to evolving your database schema while preserving data
integrity.
Chapter 8 Migrations
A migration is just a single file with SQL queries to update a database schema and apply new
changes to an existing database.
//db/data-source.ts
import { DataSource, DataSourceOptions } from "typeorm";
1. Now you don't need to register the entity manually. TypeORM will find the entities by itself.
2. When you are working with migrations you have to set the synchronize to false because our
migration file will update the changes in the database
3. You have to provide the path of migration where you want to store. I chose the dist folder. I
will run the migrations as a js file. That's why we need to build the project before running
typeorm migrations
4. We will use this data source object when we generate/run the migrations with typeorm cli
imports: [TypeOrmModule.forRoot(dataSourceOptions)];
//user.entity.ts
@Column()
phone: string
1. Install Dependencies
I am going to use an external package to generate fake/mock data.
await seedUser();
await seedArtist();
await seedPlayLists();
await manager.getRepository(User).save(user);
}
await manager.getRepository(User).save(user);
await manager.getRepository(Playlist).save(playList);
}
};
1. I have created a seedData method with an entity manager argument. I will get the repository
for each entity by calling the getRepository. This is how you can create mock data
2. I have used the Faker package to generate mock data. You can see the functions
faker.person.firstName(). You can explore methods from Faker by looking at the
documentation
@Module({
providers: [SeedService],
})
export class SeedModule {}
4.Create a SeedService
import { Injectable } from "@nestjs/common";
import { DataSource } from "typeorm";
import { seedData } from "../../db/seeds/seed-data";
@Injectable()
export class SeedService {
constructor(private readonly connection: DataSource) {}
This is an entry file, Nestjs will run this file to bootstrap the application, whenever you run the
application it will create new data and save it to DB. When you don't need new data you can disable
these two lines:
Chapter 9 Configuration
Applications often run in different environments. Depending on the environment, different
configuration settings should be used. For example, usually, the local environment relies on specific
database credentials, valid only for the local DB instance. The production environment would use a
separate set of DB credentials. Since configuration variables change, best practice is to store
configuration variables in the environment.
In Nest.js, configurations refer to the settings and parameters that define the behavior of your
application. These configurations can include various aspects, such as server settings, database
connections, API keys, logging options, and more.
Nest.js provides a flexible way to manage configurations, allowing you to easily customize the
behavior of your application based on different environments (e.g., development, production, testing)
or specific deployment scenarios. By separating configuration settings from your code, you can
make your application more portable and adaptable to different environments.
Configurations in Nest.js typically consist of key-value pairs, where each key represents a specific
setting and the corresponding value represents its configuration value. These configurations are
often stored in environment variables or configuration files.
By utilizing configurations, you can ensure that your application can be easily configured without
modifying the code itself. This makes it simpler to deploy and maintain your application in different
environments or when working with multiple teams.
Nest.js provides various mechanisms to load and use configurations, including the popular dotenv
package for loading environment variables from files, as well as custom configuration modules and
services that encapsulate the configuration logic and provide access to the configuration values
throughout your application.
Overall, configurations in Nest.js help you manage the behavior of your application in a flexible and
modular way, enabling you to adapt it to different environments and deployment scenarios without
modifying the underlying code.
Nest.js provides built-in config package for configuration. We are going to use this package. The
@nestjs/config package internally uses dotenv.
@Module({
imports: [
ConfigModule.forRoot(),
})
You have to provide the path of your env file in the ConfigModule.forRoot() method. We will have
two env files, one is for development and the other is for production level
.development.env
PORT = 3000;
.production.env
PORT = 3000;
When you want to use ConfigModule in other modules, you'll need to import it (as is standard with
any Nest module). Alternatively, declare it as a global module by setting the options object's
isGlobal property to true, as shown below. In that case, you will not need to import ConfigModule
in other modules once it's been loaded in the root module (e.g., AppModule)
//config/configuration.ts
export default () => ({
port: parseInt(process.env.PORT),
});
Step 6: Load Configuration File
ConfigModule.forRoot({
envFilePath: ['.development.env', '.production.env'],
isGlobal: true,
load: [configuration],
}),
We load this file using the load property of the options object we pass to the ConfigModule.forRoot()
method. The value assigned to the load property is an array, allowing you to load multiple
configuration files (e.g. load: [databaseConfig, authConfig]
getEnvVariables() {
return {
port: this.configService.get<number>("port"),
};
}
}
You can inject ConfigService as a dependency in any service. I wanted to show you the usage of
ConfigService in a service. To access configuration values from our ConfigService, we first need
to inject ConfigService. As with any provider, we need to import it's containing module - the
ConfigModule - into the module that will use it (unless you set the isGlobal property in the options
object passed to the ConfigModule.forRoot() method to true).
We don't need to import the ConfigModule in AuthModule because we have made it global in the
AppModule
You can get the environment variables by using the get function from ConfigService. I have
provided the type <number> and the key of the variable
Let's create the test method in the AuthController to get the env variable. I have created this route
for only testing purposes
//auth.controller.ts
@Get('test')
testEnv() {
return this.authService.getEnvVariables();
}
### TEST ENV VARIABLES
GET http://localhost:3001/auth/test
When you send a request to this URL you will get the port number in the response
//main.ts
import { ConfigService } from "@nestjs/config";
SECRET=HAD_12X#@
.production.env
SECRET=HAD_12X#@
You also have to add the new value in the configuration.ts file for the secret.
//configuration.ts
secret: process.env.SECRET,
Now we need to add the jwt secret inside the AuthModule while registering the JwtModule
//auth.module.ts
import { ConfigModule, ConfigService } from '@nestjs/config';
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('secret'),
signOptions: {
expiresIn: '1d',
},
}),
inject: [ConfigService],
}),
The registerAsync method will return the DynamicModule. In Nest.js, a dynamic module is a
feature that allows you to dynamically configure and register modules at runtime based on dynamic
conditions or external factors. It provides a way to encapsulate complex configuration logic and
allows modules to be created and registered programmatically.
Dynamic modules are useful when you have modules that require some dynamic configuration or
when you want to conditionally load modules based on runtime conditions or variables.
We have used database configuration manually, now we need to get all the configuration from the
env file. We can have a separate db configuration for the development and production environment.
You have to use your database configuration like dbName, username, and password
//data-source.ts
import { ConfigModule, ConfigService } from "@nestjs/config";
import {
TypeOrmModuleAsyncOptions,
TypeOrmModuleOptions,
} from "@nestjs/typeorm";
export const typeOrmAsyncConfig: TypeOrmModuleAsyncOptions = {
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (
configService: ConfigService
): Promise<TypeOrmModuleOptions> => {
return {
type: "postgres",
host: configService.get<string>("dbHost"),
port: configService.get<number>("dbPort"),
username: configService.get<string>("username"),
database: configService.get<string>("dbName"),
password: configService.get<string>("password"),
entities: ["dist/**/*.entity.js"],
synchronize: false,
migrations: ["dist/db/migrations/*.js"],
};
},
};
//app.module.ts
TypeOrmModule.forRootAsync(typeOrmAsyncConfig),
//configuration.ts
dbHost: process.env.DB_HOST,
dbPort: parseInt(process.env.DB_PORT),
username: process.env.USERNAME,
password: process.env.PASSWORD,
dbName: process.env.DB_NAME,
Application Configurations
Add validation for environment variables by utilizing the class-validator package in your Nest.js
application. This ensures that the application doesn't start with incorrect or missing configurations,
adhering to the "Fail-fast" principle in software engineering. Throw the validation error before the
application starts to avoid runtime issues.
# .env.development
NODE_ENV=development
# .env.production
NODE_ENV=production
enum Environment {
Development = "development",
Production = "production",
Test = "test",
Provision = "provision",
}
class EnvironmentVariables {
// 1
@IsEnum(Environment)
NODE_ENV: Environment;
@IsNumber()
PORT: number;
@IsString()
DB_HOST: string;
@IsString()
USERNAME: string;
@IsString()
PASSWORD: string;
@IsString()
DB_NAME: string;
@IsString()
SECRET: string;
}
/**
* Performs sync validation of the given object.
* Note that this method completely ignores async validations.
* If you want to properly perform validation you need to call validate method
instead.
*/
const errors = validateSync(validatedConfig, {
skipMissingProperties: false,
});
if (errors.length > 0) {
throw new Error(errors.toString());
}
return validatedConfig;
}
1. We have added the validations for all our env variables. Whenever you need to create a new
env variable in .env.development or .env.production file. You can create a validation
rule inside the EnvironmentVariables class
2. Class-transformer allows you to transform a plain object to some instance of class and
versa. Also, it allows the serialization/deserializing of objects based on criteria. This tool is
super useful on both the front end and backend.
3. You can read the documentation of class-transformer here
//app.module.ts
ConfigModule.forRoot({
validate: validate,
});
I have noticed that when you make changes in your application it will take too much time to reload
the application we can make this process faster by using webpack HMR
// eslint-disable-next-line @typescript-eslint/no-var-requires
const nodeExternals = require("webpack-node-externals");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { RunScriptWebpackPlugin } = require("run-script-webpack-plugin");
You have to specificy that you want to reload the application using webpack. Now you test the
application by making some changes and your application reload time would be faster
What is Swagger?
The components of Swagger include:
1. Swagger Specification: This forms the blueprint of the API, documenting its structure in a
format that can be both human and machine interpreted. NestJS supports Swagger by
offering packages such as @nestjs/swagger to auto-generate specifications from code,
which aligns with best practices by ensuring that the documentation remains synchronized
with the codebase.
2. Swagger Editor: This browser-based editor allows for the crafting and fine-tuning of
Swagger specifications. NestJS users can import their auto-generated Swagger
configuration into the Swagger Editor to make further refinements or visualize changes,
streamlining API design iterations.
3. Swagger UI: Interactive documentation created by Swagger UI facilitates clear
communication and understanding of API functionalities. NestJS leverages Swagger UI to
enable developers and API consumers to interact with the API endpoints directly from the
browser, encouraging a hands-on approach to exploring API capabilities.
4. Swagger Codegen: This tool provides the means to scaffold server structures and
client-side code from the API specifications. Within the NestJS framework, Swagger
Codegen can expedite development by auto-creating API-related code, thus embodying a
best practice of reducing manual coding and potential human error.
Employing Swagger within NestJS promotes a systematic, well-documented, and collaborative API
development process, reflecting best practices and serving as a foundation for robust software
architecture.
The Nest.js built-in package will be utilized to implement API documentation with Swagger. This
integration facilitates the automatic generation of interactive API documentation, which is a best
practice for maintaining clear and structured endpoint documentation for developers and users alike.
@ApiTags("auth")
export class AuthController {}
Now you will see all the auth routes in the auth section
Step 2: Add Api Operation and Response for the Signup flow
In Step 2, ApiOperation and ApiResponse annotations are added for the signup process. These
annotations, part of the Swagger module in NestJS, facilitate API documentation by describing the
operation and its possible responses, enhancing understandability for developers and end-users
alike. It is considered good practice to thoroughly document API endpoints with such annotations, as
this can significantly improve the development experience and future maintenance.
The syntax entities: ['dist/**/*.entity.js'] has been utilized for entity registration, yet it is
incompatible with Webpack hot module reloading, necessitating manual entity registration within the
typeOrmAsyncConfig object. As a best practice, specifying each entity class directly in the
TypeORM configuration ensures clarity and reliability, especially during the development phase
when hot reloading is frequently used.
@ApiProperty({
example: "Doe",
description: "provide the lastName of the user",
})
@Column()
lastName: string;
@ApiProperty({
example: "jane_doe@gmail.com",
description: "Provide the email of the user",
})
@Column({ unique: true })
email: string;
@ApiProperty({
example: "test123#@",
description: "Provide the password of the user",
})
@Column()
@Exclude()
password: string;
}
The @ApiProperty decorator enhances swagger documentation by explicitly declaring the user
schema. Utilizing this decorator is considered a best practice within NestJS for API documentation,
as it provides clear and interactive API endpoints for testing and inspection.
{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"plugins": [
{
"name": "@nestjs/swagger",
"options": {
"introspectComments": true
}
}
]
}
}
The plugin array must be created, followed by the registration of the @nestjs/swagger package as
a plugin. This step is crucial for enabling Swagger documentation in a NestJS application, a best
practice for automatically generating interactive API documentation that enhances developer
experience and API usability.
User Authentication
Step 1: Add API Operation for Login
The addition of an API operation for login is achieved through creating an authentication controller
with a dedicated login route. Implementing this within NestJS typically involves using Guards and
Strategies, leveraging Passport.js under the hood for a robust and secure authentication process. As
a best practice, it is advisable to use environment variables for sensitive information such as secret
keys and to apply validation pipelines to ensure the integrity of user input before processing
authentication.
The decision has been made to not utilize the authconstants file. The secret will be retrieved from
the env file, hence the use of process.env.SECRET is observed. NestJS recommends managing
configuration in a robust and accessible manner, and utilizing environment variables through the
ConfigModule or custom configuration services aligns with this best practice, ensuring sensitive
information is securely managed.
version: "3"
services:
mongodb:
image: mongo:latest
environment:
- MONGODB_DATABASE="test"
ports:
- 27017:27017
You start the MongoDB server by opening the terminal in your root directory and run:
docker-compose up
docker-compose down
Step 4: Connect MongoDB Database using GUI Tool
MongoDB Compass is utilized to interact with the MongoDB database. Installation is necessary for
those who do not currently have it. Download MongoDB Compass
//app.module.ts
@Module({
imports: [MongooseModule.forRoot("mongodb://localhost:27017/spotify-clone")],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
The forRoot() method in NestJS's Mongoose module requires a configuration object analogous to
the one used in mongoose.connect() from the Mongoose package. This method provides a
streamlined approach to configuring the database connection at the root module level, ensuring all
sub-modules can interact with the database consistently. A best practice entails validating the
database connection parameters and handling connection errors gracefully to maintain the stability
of the application.
Create Schema
In Mongoose, everything commences with a Schema, which maps to a MongoDB collection and
dictates the structure of the documents in that collection. Schemas serve to establish Models, with
Models being accountable for the creation and retrieval of documents from the MongoDB database.
Adopting Schemas and Models aligns with NestJS's modular approach, enhancing maintainability
and promoting adherence to an application's defined data architecture. Utilizing Mongoose with
NestJS adheres to a best practice of encapsulating database interactions, which improves code
reusability and testability.
Step 1: Create a Schema
Create a songs folder and create a schemas folder inside the songs. This structure, recommended in
NestJS, promotes a modular architecture by encapsulating schema definitions within their respective
domain context, which aligns with domain-driven design principles for maintainable code
organization.
//src/songs/schemas/song.ts
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument } from "mongoose";
@Schema()
export class Song {
@Prop({
required: true,
})
title: string;
@Prop({
required: true,
})
releasedDate: Date;
@Prop({
required: true,
})
duration: string;
lyrics: string;
}
1. The SongDocument is utilized upon injecting the Model into the SongService. It is a
NestJS-specific approach to apply TypeScript interfaces for Mongoose models, promoting
type safety and IntelliSense in the service layer.
2. Applying the @Schema() decorator designates a class as a schema definition, associating
the Song class with a MongoDB collection named songs. This decorator is part of NestJS's
Mongoose integration, which simplifies working with MongoDB by automatically pluralizing
the model name for the collection.
3. The @Prop() decorator is employed to declare a property within the document. This
decorator is crucial in defining the schema's data structure and ensuring the fields align with
the intended types in the MongoDB collection.
4. SchemaFactory is tasked with generating the bare schema definition. Employing
console.log on SongSchema will reveal the structured outcome, demonstrating the
schema's conversion to a format that Mongoose can use to enforce document structure in
MongoDB.
Schema {
obj: {
title: { required: true, type: [Function: String] },
releasedDate: { required: true, type: [Function: Date] },
duration: { required: true, type: [Function: String] }
},
paths: {
title: SchemaString {
enumValues: [],
regExp: null,
path: 'title',
instance: 'String',
validators: [Array],
getters: [],
setters: [],
_presplitPath: [Array],
options: [SchemaStringOptions],
_index: null,
isRequired: true,
requiredValidator: [Function (anonymous)],
originalRequiredValue: true,
[Symbol(mongoose#schemaType)]: true
},
releasedDate: SchemaDate {
path: 'releasedDate',
instance: 'Date',
validators: [Array],
getters: [],
setters: [],
_presplitPath: [Array],
options: [SchemaDateOptions],
_index: null,
isRequired: true,
requiredValidator: [Function (anonymous)],
originalRequiredValue: true,
[Symbol(mongoose#schemaType)]: true
},
duration: SchemaString {
enumValues: [],
regExp: null,
path: 'duration',
instance: 'String',
validators: [Array],
getters: [],
setters: [],
_presplitPath: [Array],
options: [SchemaStringOptions],
_index: null,
isRequired: true,
requiredValidator: [Function (anonymous)],
originalRequiredValue: true,
[Symbol(mongoose#schemaType)]: true
},
_id: ObjectId {
path: '_id',
instance: 'ObjectId',
validators: [],
getters: [],
setters: [Array],
_presplitPath: [Array],
options: [SchemaObjectIdOptions],
_index: null,
defaultValue: [Function],
[Symbol(mongoose#schemaType)]: true
}
},
aliases: {},
subpaths: {},
virtuals: {},
singleNestedPaths: {},
nested: {},
inherits: {},
callQueue: [],
_indexes: [],
methods: {},
methodOptions: {},
statics: {},
tree: {
title: { required: true, type: [Function: String] },
releasedDate: { required: true, type: [Function: Date] },
duration: { required: true, type: [Function: String] },
_id: { auto: true, type: 'ObjectId' }
},
query: {},
childSchemas: [],
plugins: [],
'$id': 1,
mapPaths: [],
s: { hooks: Kareem { _pres: Map(0) {}, _posts: Map(0) {} } },
_userProvidedOptions: {},
options: {
typeKey: 'type',
id: true,
_id: true,
validateBeforeSave: true,
read: null,
shardKey: null,
discriminatorKey: '__t',
autoIndex: null,
minimize: true,
optimisticConcurrency: false,
versionKey: '__v',
capped: false,
bufferCommands: true,
strictQuery: false,
strict: true
}
}
The record must be saved in the MongoDB collection, necessitating the creation of a POST endpoint
to store the songs. Utilizing NestJS's @Controller and @Post decorators, the endpoint can be
efficiently set up, showcasing the framework's streamlined approach to REST API development. It is
considered a best practice to abstract the interaction with MongoDB into a service, which allows for
cleaner controllers and easier maintenance of database operations.
@Controller("songs")
export class SongsController {
constructor(private songService: SongsService) {}
@Post()
create(
@Body()
createSongDTO: CreateSongDTO
) {
return this.songService.create(createSongDTO);
}
}
@Injectable()
export class SongsService {
constructor(
@InjectModel(Song.name) //1
private readonly songModel: Model<SongDocument> //2
) {}
async create(createSongDTO: CreateSongDTO): Promise<Song> {
const song = await this.songModel.create(createSongDTO); //3.
return song;
}
}
1. Registration of the song model within the SongsModule allows for its injection into the
service, demonstrating NestJS's modular design that promotes loose coupling and high
cohesion.
2. Utilization of the SongDocument type within the song schema file ensures that the object
adheres to the defined schema, a practice that enforces type safety and reduces runtime
errors.
3. The songModel incorporates a method to persist records in MongoDB, which illustrates the
encapsulation of database operations within models, a practice that enhances maintainability
and scalability of the application.
The MongooseModule employs the forFeature() method to configure itself, allowing specific
models to be registered within the current scope. It is a NestJS-specific mechanism that ensures
model encapsulation and modularity, a recommended approach for maintaining clean and
manageable database-related code.
POST http://localhost:3000/songs
Content-Type: application/json
{
"title": "Lasting Lover",
"releasedDate" : "2023-05-11",
"duration" :"02:33",
"lyrics": "I don't know why I can't quite get you out my sight You're always just
behind"
}
//songs.service.ts
async find(): Promise<Song[]> {
return this.songModel.find();
}
//songs.controller.ts
@Get()
find(): Promise<Song[]> {
return this.songService.find();
}
Now test this route by starting the application and sending the request at GET
http://localhost:3000/songs
@Get(':id')
findOne(
@Param('id')
id: string,
):
Promise<Song> {
return this.songService.findById(id);
}
@Delete(':id')
delete(
@Param('id')
id: string,
) {
return this.songService.delete(id);
}
Populate
This lesson covers the addition of relations between two models in Mongoose, illustrating the
association where each song is linked to a single album and each album may encompass numerous
songs. The demonstration focuses on Mongoose's populate feature, which is pivotal for managing
document relationships in MongoDB, akin to JOINs in relational databases. NestJS supports
Mongoose's features directly through its dedicated @nestjs/mongoose package, and employing
populate is a common practice to efficiently retrieve related documents.
@Schema()
export class Album {
@Prop({
required: true,
})
title: string;
1. The songs property is defined in the Album model with the type specified as an array of
MongoDB ObjectIDs, intended to store references to the IDs of the song collection within the
albums table.
2. The ref attribute establishes a reference to the songs collection, enabling the creation of
relational data structures within a MongoDB database, a feature NestJS leverages through
its Mongoose module integration.
As a best practice, it is recommended to clearly document such relationships within the code to
improve maintainability and provide clarity for future development efforts. Additionally, ensuring that
the ObjectIDs used in references are validated for their format can prevent runtime errors and
maintain data integrity.
Step 2: Add a relation in the song schema
Inclusion of a relation within the song schema can be executed through the use of decorators that
define the relationship type, such as @OneToMany or @ManyToOne, which NestJS leverages from
TypeORM. This practice encapsulates the relational aspect directly within the entity, thus promoting
a clear and maintainable structure within the application’s domain model. Best practice dictates
careful planning of entity relationships to optimize query performance and database integrity.
// song.schema.ts
@Prop({
type: Types.ObjectId,
ref: Album.name,
})
album: Album;
@Module({
imports: [
MongooseModule.forFeature([{ name: Album.name, schema: AlbumSchema }]),
],
controllers: [AlbumController],
providers: [AlbumService],
})
export class AlbumModule {}
async findAlbums() {
return this.albumModel.find().populate("songs", null, Song.name); //1
}
}
The populate method is applied on the album model to retrieve all songs associated with each
album. Utilizing this feature in NestJS effectively allows for eager loading of related entities, which
streamlines data retrieval processes. It is considered a good practice to carefully manage such
operations to optimize query performance and maintain data integrity.
//create-album-dto.ts
export class CreateAlbumDTO {
title: string;
songs: string[];
}
//album.controller.ts
@Controller("albums")
export class AlbumController {
constructor(private albumService: AlbumService) {}
@Post()
create(
@Body()
createAlbumDTO: CreateAlbumDTO
): Promise<Album> {
return this.albumService.createAlbum(createAlbumDTO);
}
@Get()
find(): Promise<Album[]> {
return this.albumService.findAlbums();
}
}
This lesson covers the deployment of a Nest.js project to Railway, detailing the necessary
configurations. It requires setting up distinct .env files for development and production
environments. Nest.js best practices include using environment-specific configuration files to
manage different settings for development, testing, and production, ensuring sensitive data is not
exposed. Proper management of environment variables is crucial for security and flexibility in
different deployment scenarios.
//configuration.ts
NODE_ENV: process.env.NODE_ENV;
Step 4: Refactor the envFile path in AppModule
A separate environment file is required for each development stage, necessitating the addition of a
dynamic environment file path. NestJS supports environment-specific configuration through its
configuration module, a best practice that ensures the separation of concerns and facilitates different
settings for development, staging, and production environments.
//app.module.ts
ConfigModule.forRoot({
envFilePath: [`${process.cwd()}/.env.${process.env.NODE_ENV}`],
isGlobal: true,
load: [configuration],
validate: validate,
}),
In this lesson, the focus is on pushing the source code to GitHub and establishing a connection
between the GitHub repository and the Railway project. As a best practice in NestJS development,
it's recommended to include a .gitignore file to prevent the versioning of environment-specific files
and node_modules, thus ensuring the repository remains clean and manageable. Additionally,
integrating continuous integration tools can streamline deployment workflows, a strategy aligned with
high-quality software development processes.
Step 1: Create a GitHub Repository
create a new GitHub repository and push the source code to that repository
Don't push the files: .env, .env.development, .env.production to your GitHub repository. Add
this as a restriction in the .gitignore
.env
.env.development
.env.production
The deployment of a Nest.js project to Railway requires configuring the project's environment
variables and settings within the Railway platform. Ensuring that the database and other services
align with the Railway deployment specifications is a critical step for smooth operation, which aligns
with best practices for cloud deployment.
DB_HOST=containers-us-west-89.railway.app
DB_PORT=5673
USERNAME=postgres
PASSWORD=PaX7MXpvWogZoxvcunkL
DB_NAME=railway
Now it is going to start deploying the project and your project will be running successfully
Step 5: Test the Application
Now you can test the application by sending the signup request from your live api
You will see this error
It means there is no users table in your Railway Postgres database. We have to migrate your
database to the production
In the next lesson, you are going to learn how to migrate the database in the production
Install DotEnv
You will get this error when you run the migration
Step 2: Set the Environment to Production using the export command
You can set the NODE_ENV to production to run the build command for the production database
Running the migration with the current setup continues to result in an error, suggesting that the env
variables in data-source.ts are not being correctly defined or accessed. A best practice in such
cases is to ensure that environment variables are being loaded properly, often by using a
configuration management library or by verifying the path and export mechanism of the environment
configuration file.
DB_HOST=containers-us-west-89.railway.app
DB_PORT=5673
USERNAME=postgres
PASSWORD=PaX7MXpvWogZoxvcunkL
DB_NAME=railway
You have to create a new .env file in the root directory of your project
Now you run the migration it will work like migration will be generated successfully
Chapter 12 Testing
What is Jest
Jest, developed by Facebook, stands as a prominent testing framework within the JavaScript
ecosystem. It finds extensive use across various JavaScript applications, fostering robust testing
practices in environments built upon React, Vue, Angular, and Node.js.
The framework encompasses a myriad of features that aid in crafting thorough tests:
1. Test Runner and Assertion APIs: Jest's integrated test runner efficiently executes tests, while
its assertion APIs allow for meticulous verification of expected results, ensuring the reliability
of tests.
2. Organized Test Suites and Matchers: Jest enables structuring tests into suites for better
organization, coupled with a diverse range of matchers for precise value assertions,
contributing to clearer and more maintainable test code.
3. Mock Functions and Spies: Its built-in mocking utilities support the creation of stand-in
functions, modules, and dependencies. Spies are also available to monitor and verify the
invocation of functions, an essential aspect of testing behavior and interactions.
4. Snapshot Testing: This unique Jest feature facilitates capturing and comparing snapshots of
component outputs or data structures, playing a pivotal role in identifying unintended
alterations, a practice that greatly enhances change detection workflows.
5. Coverage Analysis: The included code coverage tool quantifies test coverage, highlighting
untested parts of a codebase, and prompts a thorough testing discipline that strives for
comprehensive coverage.
6. Asynchronous Code Testing: The framework caters to the complexity of asynchronous
operations through dedicated utilities, streamlining the testing of promises, callbacks, and
other asynchronous patterns.
Jest's straightforward setup, combined with its execution efficiency and expansive support
documentation, positions it as a highly favored framework for JavaScript testing. Its seamless
integration with widespread JavaScript tools further solidifies its status as a foundational element of
modern JavaScript development workflows.
{
"scripts": {
"test": "jest",
"test:watch": "jest --watchAll"
},
"devDependencies": {
"jest": "^29.5.0",
"@types/jest": "^29.5.2"
}
}
1. Writing the first test in Jest involves crafting test cases that are meant to assert the expected
functionality of the application code. It is a good practice to explore various assertions that
Jest provides to cover different scenarios and edge cases.
2. Engaging with a variety of Jest assertions can ensure a comprehensive testing suite.
Effective testing strategies involve utilizing matchers like .toBe(), .toEqual(), and others
that can verify the accuracy of different data types and structures within the application.
Auto-Mocking
With automocking enabled, Jest preemptively generates mock facades for modules imported into the
test environment. This substitute of genuine modules with their mock counterparts allows for precise
control over their responses and the simulation of various test conditions.
Employing auto-mocking is advantageous for segregating the unit under test from its external
dependencies. Mocking these dependent modules ensures that the unit's functionality can be
assessed in isolation, a technique recommended for accurate unit testing.
Incorporating Auto Mocking can streamline testing of intricate systems, easing dependency
management. It shifts the focus to the unit of code in question, while interactions with its
dependencies are managed in a predictable manner, a strategy that aligns with advanced software
testing methodologies.
1. Control Behavior: Mock functions in NestJS are crafted to dictate precise behaviors and
outcomes, crucial for simulating various scenarios to evaluate code branches. They are
especially important when the real implementations involve unpredictability or
non-deterministic behavior.
2. Remove Dependencies: NestJS advocates for mocks to replace actual implementations,
thus eliminating the need for external systems or services during testing. This approach is
fundamental for creating tests that are both robust and self-contained.
3. Track Function Calls: Mock functions in a NestJS context are designed to record their usage
statistics, such as invocation count and argument details. Such information is pivotal for
verifying that components interact as expected.
4. Simplify Testing: In NestJS, mock functions streamline the testing process by emulating
elaborate or resource-intensive functions. For instance, they can mimic database calls or
external API interactions, facilitating more efficient and focused testing environments.
In NestJS, Jest is a favored testing framework where mock functions are either created via the Jest
mocking APIs or generated through Jest's auto-mocking feature. Utilizing these mock functions in
tandem with assertions is a best practice that confirms the correct behavior of components under
test.
//mock-function.spec.js
describe("Mock Function Examples", () => {
it("should create a basic mock function", () => {
const mockFn = jest.fn(); //1
// mockFn.mockReturnValue('HELLO WORLD');
mockFn.mockReturnValue(3); //2
console.log(mockFn());
expect(mockFn()).toBe(3);
expect(mockFn.mock.calls.length).toBe(2); //3
expect(mockFn).toHaveBeenCalled();
});
});
1. An empty mock function has been created. This serves as a placeholder in testing
environments to simulate the behavior of real functions without implementing their logic.
2. The function is configured to return a value of 3. Returning a static value is a common
practice for simplifying tests, ensuring that the output is predictable and the function's
behavior is consistent.
3. Tracking the calls to mock functions is achievable. This allows for the verification of
interactions within the code, ensuring that functions are invoked as expected, which aligns
with the best practices of test-driven development.
Analogously, think of a mock function as a stunt double in a film: it performs the actions required for
the scene (test case) without being the actual actor (real function), and its performance can be
precisely controlled and reviewed.
You can also pass an argument to the mock function just like we did createSongDTO
It is considered a best practice to isolate and mock external dependencies in tests to verify that the
system's reaction to various potential responses is handled correctly. Analogous to a flight simulator
for pilots, this technique allows developers to safely test the behavior of the application in a
controlled environment before deployment.
it("it should create a mock function with promise", () => {
const mockFetchSongs = jest.fn();
mockFetchSongs.mockResolvedValue({ id: 1, title: "Dancing Feat" }); //1
mockFetchSongs().then((result) => {
console.log(result);
});
expect(mockFetchSongs).toHaveBeenCalled();
expect(mockFetchSongs()).resolves.toEqual({ id: 1, title: "Dancing Feat" });
//2
});
1. The operation will resolve to a promise that yields the object { id: 1, title: 'Dancing
Feat' }. This approach is aligned with best practices, encapsulating the asynchronous
nature of operations which may involve I/O processes, such as database interactions.
2. To verify the resolution of a promise, one can utilize the resolves matcher in testing
frameworks. This facilitates the writing of clean, robust tests that ensure promises are fulfilled
as expected, which is an essential aspect of reliable software delivery.
SpyOn Functionality
Spies play a crucial role in testing scenarios where there is a need to monitor and manipulate the
behavior of function calls. They enable the validation of how functions are used without altering their
actual implementation, aligning with best practices for maintaining clean and reliable test suites.
The jest.spyOn() function in Jest is employed to construct a spy for any method of an existing
object, allowing for a granular control and assertion of that method's interactions. This utility provides
the advantage of asserting calls and responses, facilitating a mock implementation when necessary,
which is particularly useful when needing to simulate real-world scenarios in a controlled test
environment.
Incorporating spies into unit tests by using jest.spyOn() is analogous to deploying surveillance
equipment; it discreetly observes the interactions without obstructing the
const songRepository = {
create: (createSongDTO) => {
// Original method implementation
},
};
// Assertions
expect(spy).toHaveBeenCalled(); //3
expect(spy).toHaveBeenCalledWith({ id: 1, title: "Lover" });
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(1);
1. A mock function has been implemented for the create property in the SongRepository to
simulate functionality without invoking the actual implementation. Utilizing spyOn provides
the additional benefit of creating a mock function while also monitoring the call activities,
which aligns with best practices for testing where monitoring interactions with certain
components is essential.
2. Upon invoking songRepository.create, the underlying mechanism defers to the spy
function equipped with the bespoke fake implementation. This approach ensures that tests
can verify that the repository's create function is being called as expected, a technique
considered a best practice to validate the integration point without relying on the actual
database operation, enhancing test reliability and execution speed.
By employing these techniques, one mimics the delicate workings of a timepiece, ensuring each
gear (or function) interacts correctly with the others, while still allowing for inspection of each
individual movement (or function call).
A best practice is to use spies from robust testing libraries like Jest, which can keep track of calls to
the method, arguments passed, and even allow specifying return values. This strategy ensures that
unit tests remain isolated and are not affected by external factors, much like using a test dummy in
car crash simulations to understand impacts without risking actual harm.
An additional tip is to always restore or clean up spies after each test to prevent unexpected
behavior in subsequent tests. This is similar to resetting a chessboard after a game; it ensures that
the starting conditions are consistent every time.
class ArtistRepository {
save(createArtistDTO) {
// Original method implementation
}
}
it("should spy on the class method", () => {
const artist = new ArtistRepository();
const spy = jest
.spyOn(artist, "save")
.mockImplementation((createArtistDTO) => createArtistDTO);
1. Invoking artist.save engages the implementation of the spy function, allowing for
monitoring of function calls. The spy function accepts createArtistDTO as an argument
and returns the identical createArtistDTO, enabling the confirmation of data integrity and
flow through the function.
Best practice dictates the use of such spy functions in test cases to ensure that methods are called
with the correct arguments, effectively simulating the behavior of complex dependencies. Analogous
to a checkpoint in a relay race, it verifies the baton (in this case, createArtistDTO) is passed
correctly, ensuring the next runner (function or method in the application) can proceed without
issues.
Removing the manual mockRestore calls from each test case and utilizing jest.resetAllMocks
within the afterEach hook streamlines the testing process. This approach ensures that all mocks
are reset automatically after each test, promoting cleaner test architecture and reducing the chance
of state leakage between tests, much like a blackboard is wiped clean after each lesson to ensure a
fresh start for new information.
Incorporating this method is considered a best practice, as it adheres to the principle of test isolation,
akin to how surgeons sterilize their tools after each procedure to prevent cross-contamination.
Additionally, this tactic enhances test reliability and maintainability, simplifying future updates to the
test suite.
Unit Testing
Unit testing represents a pivotal software testing methodology where the smallest testable parts of
an application are evaluated independently. It is designed to affirm that each code segment, be it
functions, methods, or classes, operates as intended, delivering the correct outcomes for distinct
inputs.
1. Unit tests facilitate the early discovery of issues, typically during the development phase.
This preemptive measure allows for the rectification of problems prior to their expansion
throughout the software, thereby streamlining the development process.
2. The application of unit tests enhances code integrity and fosters the development of code
that is both modular and maintainable. Such tests instill confidence in the functionality of
code segments and serve as a bulwark against future regressions when the codebase
evolves.
3. When a unit test signals a failure, it precisely pinpoints the troubled segment, significantly
expediting the debugging process. This targeted approach to problem-solving is integral to
efficient software maintenance.
4. As code undergoes refactoring, unit tests provide a framework that assures the consistency
of unit functionality. They are the custodians of code behavior, ensuring that modifications do
not inadvertently affect existing features.
In addition, it’s advisable to keep the test files named consistently with their corresponding modules.
This convention simplifies the identification and execution of related tests, much like arranging books
by genre simplifies the search in a library.
describe("SongController", () => {
let controller: SongController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SongController],
}).compile(); //1
1. Nest.js automatically generates a fake testing module. Upon execution of the compile
function, it instantiates all dependencies required for the testing module, ensuring a
controlled environment akin to a laboratory where experiments can be conducted in isolation.
2. Obtaining an instance of the controller dependency is a straightforward process. It involves
using the testing module’s get method, which serves as a direct line to the desired
dependencies, much like retrieving a specific book from a well-organized shelf using a
catalog.
When the test is executed, the error message is revealed. This error message acts as an early
indicator, a lighthouse warning ships of potential danger, enabling developers to pinpoint the fault
and implement corrective measures. As a best practice, thorough inspection and understanding of
the error message are crucial; it should be seen as a map leading to the bug that needs to be fixed.
It's advisable to treat tests as first-class citizens, crafting them with the same care as production
code, and reviewing error messages from tests with a keen, diagnostic eye to maintain robustness in
the application.
SongController
Nest can't resolve dependencies of the SongController (?). Please make sure that
the argument SongService at index [0] is available in the RootTestModule context.
Potential solutions:
- Is RootTestModule a valid NestJS module?
- If SongService is a provider, is it part of the current RootTestModule?
- If SongService is exported from a separate @Module, is that module imported
within RootTestModule?
@Module({
imports: [ /* the Module containing SongService */ ]
})
When you check the SongController it is dependent on the SongService. We have to unit test the
controller functions individually. That's why we need to mock the SongService to individually test the
controller functions
controller = module.get<SongController>(SongController);
service = module.get<SongService>(SongService);
The useValue syntax in NestJS's dependency injection system allows for substituting real
implementations with fake ones, useful for testing purposes. For instance, when configuring a
provider in a module, setting useValue to an object with methods mirroring those of SongService
can simulate database interactions, aligning with the best practice of using fakes or mocks during
unit testing to ensure tests run quickly and reliably without real database dependencies.
As an analogy, using useValue is like using a stunt double in a movie; it stands in for the real actor
(the SongService), performing necessary actions during testing scenes, which allows the real
service to remain unaffected and free from the potential side effects of rigorous testing procedures.
This approach is an elegant solution to maintaining a controlled test environment.
describe("createSong", () => {
it("should create a new song", async () => {
const newSongDTO: CreateSongDTO = {
title: "Runaway",
};
const song = await controller.createSong(newSongDTO);
expect(song.title).toBe("Runaway");
});
});
describe("updateSong", () => {
it("should update the song DTO", async () => {
const updatesongDTO: UpdateSongDTO = {
title: "Animals",
};
const updateResults = await controller.updateSong("a uuid", updatesongDTO);
expect(updateResults).toBeDefined();
expect(updateResults.affected).toBe(1);
});
});
describe("deleteSong", () => {
it("should delete the song", async () => {
const deleteResult = await controller.deleteSong("a uuid");
expect(deleteResult.affected).toBe(1);
});
});
describe("SongService", () => {
let service: SongService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [SongService],
}).compile();
service = module.get<SongService>(SongService);
});
Now there's a pattern that's established for testing. SongService needs similar testing help as the
controller.
Nest can't resolve dependencies of the SongService (?). Please make sure that
the argument SongRepository at index [0] is available in the RootTestModule
context.
Potential solutions:
- Is RootTestModule a valid NestJS module?
- If SongRepository is a provider, is it part of the current RootTestModule?
- If SongRepository is exported from a separate @Module, is that module
imported within RootTestModule?
@Module({
imports: [ /* the Module containing SongRepository */ ]
})
6 |
7 | beforeEach(async () => {
> 8 | const module: TestingModule = await Test.createTestingModule({
| ^
9 | providers: [SongService],
10 | }).compile();
11 |
Now there's the same problem as the conroller. To fix this, create the mock SongRepository to run
the test for SongService.
service = module.get<SongService>(SongService);
repo = module.get<Repository<Song>>(getRepositoryToken(Song));
Step 3: Test SongService
it("should give me the song by id", async () => {
const song = await service.getSong("a uudi");
const repoSpy = jest.spyOn(repo, "findOneOrFail");
expect(song).toEqual(oneSong);
expect(repoSpy).toBeCalledWith({ where: { id: "a uudi" } });
});
1. Integration issues become apparent through E2E testing, as it uncovers problems that
surface during the interaction between disparate application segments. Adopting this testing
strategy is a best practice for identifying data inconsistencies, communication breakdowns,
and API compatibility issues, which are often obscured within the confines of unit and
integration testing stages.
2. E2E testing is instrumental in validating user journeys, ensuring that the application reliably
navigates the critical pathways a user might traverse. It acts as a safeguard, confirming that
the software responds with the correct outputs across a multitude of user interactions, which
is a testament to meticulous software craftsmanship.
3. The user experience is enhanced when E2E testing is employed, as it encompasses a
holistic view of the application's operation. This approach is akin to a conductor overseeing
an orchestra, ensuring every section performs harmoniously, resulting in a seamless
performance—mirrored in the software by uniform navigation, fluid data transactions,
accurate UI representations, and responsive user engagements.
4. The deployment process is fortified with E2E testing, functioning as the final rehearsal before
the live performance of the application in the production environment. It instills a level of
assurance that all systems operate as expected, analogous to a dress rehearsal in theater,
ensuring that every act and scene transitions flawlessly before the curtain rises.
In Nest.js, a watch script for end-to-end (E2E) testing is not part of the default setup. For continuous
feedback during development, it is recommended to implement a watch mechanism, such as using
Jest's --watch flag, to automatically re-run E2E tests when changes are detected.
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
url: 'postgres://postgres:root@localhost:5432/test-dev',
synchronize: true,
entities: [Song],
dropSchema: true,
}),
SongModule,
],
}).compile();
Ensuring the creation of a separate database for application testing is essential. Setting the
dropSchema option to true within the TypeORM configuration ensures the schema is automatically
dropped post-testing, maintaining a clean state.
Step 3: Clear SongRepository
afterEach(async () => {
// Fetch all the entities
const songRepository = app.get("SongRepository");
await songRepository.clear();
});
When testing Get Songs endpoints, the approach includes simulating client requests and asserting
the expected responses, which should reflect the retrieval of song data. It's a common practice to
mock the service layer to return predefined data, thereby isolating the controller's response logic for
accurate and efficient validation.
The supertest package, pre-installed in NestJS, facilitates the testing of HTTP APIs by simulating
server behavior and handling HTTP requests and responses. This allows for the creation of robust
test suites that ensure APIs behave as expected under various conditions, adhering to best practices
for maintaining high-quality software standards.
Step 5: Test GET Song Endpoint
When conducting tests for the GET Song endpoint, it is essential to ensure that the request retrieves
the correct song data and handles potential errors gracefully. The test should mimic a client's
request for a song, verifying that the endpoint returns the expected song data with the correct HTTP
status code.
A best practice in this context is to utilize mock repositories or in-memory databases, which provide
isolation for tests, thus preventing side effects on the actual database and ensuring that each test
case runs in a consistent state. This technique not only accelerates testing procedures but also
maintains the integrity of test scenarios.
it("/POST songs", async () => {
const createSongDTO = { title: "Animals" };
const results = await request(app.getHttpServer())
.post(`/songs`)
.send(createSongDTO as CreateSongDTO);
expect(results.status).toBe(201);
expect(results.body.title).toBe("Animals");
});
Best practices dictate the utilization of TypeORM's transactional operations during testing to maintain
database integrity, which allows for each test to run in isolation without impacting the database state.
Additionally, incorporating service mocks within the test suite is recommended to simulate
interactions with the database or external services, ensuring tests are fast and reliable.
Nest.js version 10 introduces enhancements to the compiler speed, resulting in accelerated project
execution times. This advancement leads to a more efficient development experience when building
Nest.js applications, as faster compilation directly improves the feedback loop for developers.
Best practices suggest leveraging such improvements to compiler speed by regularly updating to the
latest version, ensuring that the development environment is as performant as possible. Additionally,
it's advisable to monitor the project's build times after upgrading, to quantify the improvement and
identify any areas where the newer version can be further optimized for speed.
"compilerOptions": {
"builder": "swc",
"typeCheck": true
}
First of all, install these two dependencies to work with the websocket.
// Events.module.ts
import { Module } from "@nestjs/common";
import { EventsGateway } from "./events.gateway";
@Module({
providers: [EventsGateway],
})
export class EventsModule {}
@WebSocketGateway({
cors: {
origin: "*",
},
})
export class EventsGateway implements OnModuleInit {
@WebSocketServer()
server: Server;
onModuleInit() {
//3
this.server.on("connection", (socket) => {
console.log(socket.id);
console.log(socket.connected);
});
}
// You can listen to this event
// Client can send message to me by using the message key/event name
@SubscribeMessage("message")
newMessage(
@MessageBody()
data: any
) {
console.log("Message is received from the client");
console.log(data); //Here we did not send a message back to the server
// Save the message to the database and return the reply in the response
// Call the service method to save the record in the DB
}
}
Gateways operate on an event-driven architecture, where events are emitted and consumed by
clients and the server. Clients can listen for specific events and the server can emit events to trigger
actions on the client side.
The @MessageBody() decorator is applied to the data parameter. This decorator instructs Nest.js to
extract the message payload from the incoming WebSocket message and bind it to the data
parameter.
A best practice in this scenario is to ensure robust error handling and validation of incoming
messages to prevent potential issues stemming from malformed data. It's also advisable to structure
the code for easy maintenance and scalability, possibly by abstracting the event handling into
separate, testable functions or classes.
<html>
<head>
<body>
<p>Please check the console for the message reply</p>
</body>
<script
src="https://cdn.socket.io/4.3.2/socket.io.min.js"
integrity="sha384-KAZ4DtjNhLChOB/hxXuKqhMLYvx3b5MlT55xPEiNmREKRzeEm+RVPlTnAn0ajQN
s"
crossorigin="anonymous"
></script>
<script>
const socket = io('http://localhost:3000');
socket.on('connect', function () {
console.log('Connected');
// Send Message to Websocket Server
socket.emit('message', { msg: 'THIS IS THE MESSAGE FROM THE CLIENT' });
});
<body></body>
</html>
As a best practice, it's advisable to leverage the RxJS library, which Nest.js integrates seamlessly
with, to create Observables. This enables fine-grained control over event handling, allowing for the
composition of sophisticated data handling and transformation strategies, essential for robust and
reactive application architectures.
@SubscribeMessage('message')
newMessage(
@MessageBody()
data: any,
): Observable<WsResponse<any>> { //1
console.log('Message is receieved from the client');
console.log(data);
return of({ event: 'message', data: 'Learn Node' });
}
Summary
In the context of Nest.js, the integration of Socket.IO for WebSocket communication is vital for
real-time, bi-directional communication between web clients and servers. The setup involves
establishing a persistent connection that allows clients to send messages to the server and listen for
responses. Best practices include handling exceptions gracefully and ensuring that disconnection
events are managed to maintain robust communication. Additionally, logging received data to the
console is a useful technique for monitoring and debugging the flow of messages. When
implementing this in Nest.js, one would typically encapsulate WebSocket management within a
dedicated module or service, leveraging Nest.js's dependency injection to manage lifecycle events
and provide a clean separation of concerns.
@apollo/server: This package is part of the Apollo GraphQL ecosystem and provides a GraphQL
server implementation. It allows you to create a GraphQL server using Node.js, enabling you to
define your GraphQL schema, and resolvers, and handle incoming GraphQL queries and mutations.
@nestjs/graphql: This package is another part of the NestJS ecosystem and provides GraphQL
support for NestJS applications. It allows you to define GraphQL schemas, and resolvers, and use
decorators to annotate your classes and methods to create GraphQL endpoints. It works in
conjunction with @apollo/server or other GraphQL server implementations.
graphql: This package is the core GraphQL library that provides the fundamental building blocks for
working with GraphQL. It includes utilities for parsing, validating, and executing GraphQL queries
and mutations. It is a widely used package in the GraphQL community and is required by most
GraphQL server implementations.
ts-morph: This package is a TypeScript AST (Abstract Syntax Tree) manipulation library. It allows
you to programmatically analyze, modify, and generate TypeScript code. It can be particularly useful
when working with code generation or refactoring tools, such as generating TypeScript types from
GraphQL schemas or modifying existing TypeScript files programmatically.
In summary, the packages we mentioned are commonly used in the context of building GraphQL
servers with NestJS and Apollo. They provide the necessary tools and integrations to define
GraphQL schemas, handle incoming queries and mutations, and manipulate TypeScript code when
needed.
type Song {
id: ID!
title: String!
}
type Query {
songs: [Song!]!
}
Utilizing this schema, one can execute GraphQL queries to retrieve data regarding songs. The sole
query operation provided is songs, yielding an array of song objects, with each object including
properties such as ID and title.
Best practices recommend ensuring that each query is optimized for performance, possibly by
leveraging indexes on the ID field to expedite lookup times. Additionally, implementing proper error
handling and security measures, such as validation and authentication on queries, can protect
against inefficient data retrieval and unauthorized access.
For example, to retrieve all the songs, you can execute the following GraphQL query:
query {
songs {
id
title
}
}
The query retrieves all songs from the GraphQL server, delivering both IDs and titles to the client.
For the query to function in a production environment, resolvers must be developed to establish the
methods of data retrieval upon execution of the songs query, which is not depicted in this schema
outline.
Best practices suggest the implementation of resolvers should be designed to be efficient and
scalable, often including pagination and filtering capabilities to handle large datasets. Additionally,
implementing caching mechanisms can significantly enhance performance by reducing redundant
database calls for frequently requested data.
A best practice in this process involves ensuring that the script within generate-typings.ts is both
maintained with the current project's structure and executed as part of the build process. This
maintains type accuracy, aiding in early detection of potential type mismatches or errors during
development.
In the context of Nest.js, when defining a resolver for GraphQL, specific types must be declared to
ensure the proper resolution of queries and mutations. It is essential to accurately define the types to
map the application's data structures to the GraphQL schema, facilitating clear and type-safe data
exchange.
A best practice includes leveraging TypeScript's strong typing system within Nest.js to reinforce the
integrity of GraphQL resolvers. By doing so, one ensures that the code aligns with the defined
schema, reducing the potential for runtime errors and improving maintainability.
You have to add a script in the package.json file. You can run this command by executing this
script
type Query {
song(id: ID!): Song!
}
In the context of a NestJS application, extending the GraphQL schema to include queries for specific
items, such as songs, enhances the API's capabilities. Such an extension allows clients to retrieve a
single song using its unique identifier, which optimizes data retrieval by fetching only the required
item rather than the entire collection.
It is a widely accepted best practice to provide such granular query capabilities, as it can significantly
improve the performance of the application by reducing the amount of data transferred over the
network and the load on the server. Additionally, crafting precise and efficient queries is a hallmark of
well-designed GraphQL services, allowing for flexible and optimized client-server interactions.
query {
song(id: "123") {
id
title
}
}
type Mutation {
createSong(title: String!): Song!
updateSong(id: ID!, updateSongInput: UpdateSongInput!): UpdateResult!
deleteSong(id: ID!): DeleteResult!
}
type UpdateSongInput {
title: String
}
type UpdateResult {
affected: Int!
}
type DeleteResult {
affected: Int!
}
In NestJS, mutations in the schema reflect the capability to alter data within the application,
encapsulating operations such as creating, updating, and deleting records. These mutation
operations are critical for maintaining dynamic data states within the application and directly
correspond to CRUD (Create, Read, Update, Delete) operations that are fundamental to persistent
storage management.
Employing the createSong, updateSong, and deleteSong mutations allows for granular control
over the song records, with the operations returning types UpdateResult and DeleteResult that
furnish insights into the result of these mutations. As a best practice, ensuring these mutations are
well-tested and transactional can help maintain data integrity and provide rollback mechanisms in
case of failures. This provides robustness to the data management layer of a NestJS application.
As a standard procedure, generating typings through the npm run generate:typings command
ensures that the GraphQL schema changes are reflected in the TypeScript types. This step helps to
maintain type safety across the application, preventing runtime errors due to type mismatches and
facilitating a robust development environment.
input UpdateSongInput {
title: String
}
@Resolver()
export class SongResolver {
constructor(private readonly songService: SongService) {}
@Query("songs")
async getSongs(): Promise<Song[]> {
return this.songService.getSongs();
}
}
Implementing such methods following the Single Responsibility Principle ensures that each function
performs a specific task, improving maintainability and testability. Including comprehensive error
handling within service methods helps to gracefully manage exceptions, ensuring reliable application
behavior.
@Query('song')
async getSong(
@Args('id')
id: string,
): Promise<Song> {
return this.songService.getSong(id);
}
input CreateSongInput {
title: String!
}
Best practices suggest that mutation operations should be explicit in their intent and tightly scoped to
ensure maintainability and clear understanding of their impact. Furthermore, the use of DTOs (Data
Transfer Objects) like UpdateSongDTO for encapsulating the update data is recommended to enforce
validation and encapsulation, which helps to maintain the integrity of the data being sent to the
server.
@Mutation('updateSong')
async updateSong(
@Args('updateSongInput')
args: UpdateSongDTO,
@Args('id')
id: string,
): Promise<UpdateResult> {
return this.songService.updateSong(id, args);
}
@Mutation('deleteSong')
async deleteSong(
@Args('id')
id: string,
): Promise<DeleteResult> {
return this.songService.deleteSong(id);
}
Error Handling
Error handling is a crucial aspect of developing resilient applications, and within the context of a
NestJS GraphQL service, throwing an error from a resolver, such as getSongs in SongResolver,
allows for graceful failure management. This approach can encapsulate business logic validation
and operational faults, providing clear feedback to the client about what went wrong.
Incorporating error handling at the resolver level adheres to best practices by localizing error
management and maintaining clean separation of concerns. Structured correctly, it can streamline
debugging and maintenance, ensuring that the GraphQL API communicates effectively with clients
and enhances the overall robustness of the service.
@Query('songs')
async getSongs(): Promise<Song[]> {
// return this.songService.getSongs();
// throw new Error('Unable to fetch songs!');
throw new GraphQLError('Unable to fetch the songs', {
extensions: {
code: 'INTERNAL_SERVER_ERROR',
},
});
}
In practice, structured error handling is crucial as it aids client applications in deciphering the nature
of errors. The inclusion of an error code within the extensions of GraphQLError allows for consistent
error processing and can be used to trigger specific client-side behavior. It's recommended to handle
errors gracefully and provide meaningful error messages to maintain a seamless user experience.
Moreover, logging such errors in a manner that they can be monitored and analyzed can lead to
improved system robustness and quicker resolution of underlying issues.
Chapter 15 Authenticate GraphQL APIs
Error handling is a crucial aspect of developing resilient applications, and within the context of a
NestJS GraphQL service, throwing an error from a resolver, such as getSongs in SongResolver,
allows for graceful failure management. This approach can encapsulate business logic validation
and operational faults, providing clear feedback to the client about what went wrong.
Incorporating error handling at the resolver level adheres to best practices by localizing error
management and maintaining clean separation of concerns. Structured correctly, it can streamline
debugging and maintenance, ensuring that the GraphQL API communicates effectively with clients
and enhances the overall robustness of the service.
@Query('songs')
async getSongs(): Promise<Song[]> {
// return this.songService.getSongs();
// throw new Error('Unable to fetch songs!');
throw new GraphQLError('Unable to fetch the songs', {
extensions: {
code: 'INTERNAL_SERVER_ERROR',
},
});
}
In practice, structured error handling is crucial as it aids client applications in deciphering the nature
of errors. The inclusion of an error code within the extensions of GraphQLError allows for consistent
error processing and can be used to trigger specific client-side behavior. It's recommended to handle
errors gracefully and provide meaningful error messages to maintain a seamless user experience.
Moreover, logging such errors in a manner that they can be monitored and analyzed can lead to
improved system robustness and quicker resolution of underlying issues.
The generation of TypeScript typings is an essential step for enforcing type safety within the
application, which facilitates the early discovery of errors and enhances the development
experience. Implementing a script to automate the generation of these typings ensures consistency
and efficiency, which is a recommended practice for maintaining a robust and scalable codebase.
Best practices include implementing robust security measures within the resolver, such as password
hashing and token-based authentication, to protect user data. Additionally, it's advantageous to
employ middleware for consistent session management across the application, ensuring a secure
and seamless user experience.
For best practices, the resolver functions must be designed with security as a priority, using practices
such as password hashing and validation checks. Additionally, leveraging features such as guards
and interceptors can optimize the authentication flow and enhance the application's security posture.
It is also recommended to modularize the resolver code for maintenance and readability, separating
different functionalities into distinct, manageable units.
@Resolver()
export class AuthResolver {
constructor(
private userService: UsersService,
private authService: AuthService
) {}
@Mutation("signup")
async singupUser(
@Args("signupInput")
signupInput: SignupInput
): Promise<SignupResponse> {
return this.userService.create(signupInput);
}
@Query("login")
async loginUser(
@Args("loginInput")
loginInput: LoginInput
): Promise<LoginResponse> {
return this.authService.login(loginInput);
}
}
Now run the application and test the signup mutation and login request.
type Profile {
email: String!
userId: String!
}
Defining new TypeScript typings via a dedicated script command enhances the robustness of the
codebase by ensuring that data structures are predictable and type checks are enforceable at
compile time. It is a sound practice to include this step in continuous integration pipelines for
automated checks.
Securing a resolver in NestJS, such as a user profile resolver, is crucial to safeguard sensitive data
and ensure that only authenticated users can access their profiles. Implementing a custom
AuthGuard is a strategic approach to enforcing authentication and authorization policies on specific
application routes or handlers, maintaining the integrity of user data. Integrating guards into the
application's lifecycle allows for scalable and manageable security mechanisms, which can be
tailored to the evolving needs of the application.
@Query('profile')
@UseGuards(GraphQLAuthGaurd)
getProfile(parent, args, contextValue, info): Profile {
console.log(parent);
console.log(args);
console.log(contextValue);
console.log(info);
return contextValue.req.user;
}
The getProfile function showcased is a query resolver in a GraphQL API built with NestJS,
guarded by a custom GraphQLAuthGaurd. This resolver retrieves the user profile from the context of
an authenticated request, where contextValue.req.user is typically populated by the
authentication middleware after a user successfully logs in.
Best practices for such resolvers include implementing robust logging strategies to facilitate
debugging and introspection, while also ensuring sensitive information is not logged in production
environments. Additionally, thorough checks and validation on args and contextValue should be in
place to maintain the integrity of the data and the security of the API.
As a robust practice, this guard should be thoroughly tested to guarantee it behaves as expected
under various scenarios, thereby reinforcing the security posture. It is also advisable to keep the
guard's logic decoupled and modular, allowing for reuse across different parts of the application as
security requirements evolve.
@Injectable()
export class GraphQLAuthGaurd extends AuthGuard("jwt") {
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
const ctx = GqlExecutionContext.create(context);
const { req } = ctx.getContext();
return super.canActivate(new ExecutionContextHost([req]));
}
This custom guard overrides the canActivate and handleRequest methods, facilitating the integration
of the authentication guard with the GraphQL execution context. By ensuring the authentication
flows seamlessly with GraphQL's unique context handling, the application upholds security across
GraphQL resolvers. As a best practice, it is critical to handle errors gracefully and provide
informative messages, as demonstrated in the handleRequest method. Additionally, utilizing
Observables for asynchronous operations offers a robust approach to handle streams of
authentication events, ensuring a reactive system that can scale with the application's needs.
As a best practice, the access token should be handled securely, often sent in the HTTP
Authorization header, and validated on the server for each request to protect against unauthorized
access. Utilizing middleware or guards in NestJS to automate the token validation process can
streamline security and ensure consistency across the application.
Subscriptions in GraphQL
Incorporating this package into a project follows the best practice of enhancing user experience with
dynamic content updates, without the need for manual refreshes. It's advisable to ensure that the
use of subscriptions is optimized and does not strain the server with unnecessary loads, and proper
authorization mechanisms are implemented to secure real-time data channels.
GraphQLModule.forRoot<ApolloDriverConfig>({
installSubscriptionHandlers: true,
}),
It is considered good practice to encapsulate the WebSocket configuration within dedicated modules
or providers, thus keeping the AppModule clean and maintainable. Additionally, thorough testing of
WebSocket handlers is recommended to ensure that they handle client communication effectively
and securely.
@Subscription('songCreated')
songCreated() {
return pubSub.asyncIterator('songCreated'); //1
}
The songCreated field has been established within the subscription. Upon running the application
and initiating the subscription, it actively listens and awaits the event to be published. The
AsyncIterator specifically listens for events associated with a designated label, facilitating
event-driven workflows in the application.
Step 4: Publish the event
const newSong = this.songService.createSong(args);
pubSub.publish("songCreated", { songCreated: newSong });
return newSong;
The createSong resolver is configured to publish an event, with songCreated being the designated
payload. Observers of this event can expect to receive the songCreated payload as demonstrated
in the response provided below.
Chapter 16 Testing GraphQL APIs
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [SongResolver],
}).compile();
resolver = module.get<SongResolver>(SongResolver);
});
Running the test file in question will result in an error. This indicates a potential issue with the
configuration or the environment setup for the tests, which necessitates investigation and correction
to proceed with successful test execution.
SongResolver
✕ should be defined (34 ms)
Nest can't resolve dependencies of the SongResolver (?). Please make sure
that the argument SongService at index [0] is available in the RootTestModule
context.
Potential solutions:
- Is RootTestModule a valid NestJS module?
- If SongService is a provider, is it part of the current RootTestModule?
- If SongService is exported from a separate @Module, is that module imported
within RootTestModule?
@Module({
imports: [ /* the Module containing SongService */ ]
})
providers: [
SongResolver,
{
provide: SongService,
useValue: {
getSongs: jest
.fn()
.mockResolvedValue([{ id: 'a uuid', title: 'Dancing Feat' }]),
getSong: jest.fn().mockImplementation((id: string) => {
return Promise.resolve({ id: id, title: 'Dancing' });
}),
createSong: jest
.fn()
.mockImplementation((createSongInput: CreateSongInput) => {
return Promise.resolve({ id: 'a uuid', ...createSongInput });
}),
updateSong: jest
.fn()
.mockImplementation((updateSongInput: UpdateSongInput) => {
return Promise.resolve({ affected: 1 });
}),
The practice of crafting mock implementations for services within controllers is a strategic step for
testing and development purposes. By simulating the behavior of the SongService in the
SongController, one can validate the interaction and logic without the need for the actual service
to be implemented or available.
A well-constructed test for a resolver, like getSongs, not only assesses the returned value against
expected results but also encapsulates the performance of underlying services and components. It's
a proactive measure to guard against regressions in the API's behavior, especially after updates or
refactoring, ensuring that the end-user's queries receive consistent and accurate responses.
The integration of a watch mode for E2E tests into the development workflow can significantly
improve code quality by enabling developers to identify and resolve integration issues as they code.
It acts as a real-time safeguard against regressions and defects, which is a reflection of high
standards in software development and maintenance.
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: "postgres",
url: "postgres://postgres:root@localhost:5432/test-dev",
synchronize: true,
entities: [Song],
dropSchema: true,
}),
SongModule,
],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
afterEach(async () => {
const songRepository = app.get("SongRepository");
await songRepository.clear();
});
afterAll(async () => {
await app.close();
});
To ensure test accuracy and environment isolation, the Songs table in the database must be cleared
after each test is executed. This practice prevents state leakage between tests and maintains
consistency.
The file to initiate End-to-End testing in watch mode is executed with the command npm run
test:e2e:watch. This initiates a process that continually monitors for any changes in the codebase
and reruns tests accordingly, allowing for immediate feedback and efficient debugging.
expect(results.statusCode).toBe(200);
expect(results.body).toEqual({ data: { songs: [newSong] } });
});
1. Testing queries in Nest.js, especially when involving a database, begins by ensuring that the
relevant records exist to be queried against. The setup phase of a test typically includes
invoking methods like createSong to insert necessary data into the Songs table, resulting in
a controlled and predictable testing environment.
2. The formation of GraphQL queries is facilitated through the use of structured variables, such
as queryData, which hold the GraphQL query string. These queries are then utilized to
interact with the GraphQL API, which, by convention, operates under a single endpoint,
typically /graphql.
3. To execute these queries within a test suite, the query data must be provided to the send
method of the testing tool. This simulates a client request to the server, allowing for the
testing of query handling and response accuracy.
4. In high-quality software engineering, a tip is to ensure the testing environment closely mimics
production conditions without affecting real data, maintaining isolation between tests for
reliability. Additionally, structuring test cases to reflect a variety of realistic scenarios can
uncover edge cases and potential bugs before deployment.
1. Defining a Query in GraphQL with specific operation names and required variables, such as
GetSong($id: ID!), clarifies the expected input and makes the query reusable. Variables
in GraphQL, like $id, are used to parameterize queries, allowing for dynamic input that can
be varied with each query execution.
2. Supplying the $id variable within a song query allows for the specification of which particular
song is being requested. This is a part of query composition in GraphQL, where the query
variables are declared and then used within the query body to fetch data.
3. The variables section in a GraphQL operation is where the actual values for the declared
variables are specified. This is where one sets the specific ID value for $id when executing
the query, thereby allowing for the retrieval of data corresponding to that particular identifier.
expect(results.body.data.createSong.title).toBe("Animals");
});
expect(results.body.data.updateSong.affected).toBe(1);
});
expect(results.body.data.deleteSong.affected).toBe(1);
});
For optimal results, these tests would typically include checks for handling of invalid inputs and edge
cases to ensure resilience. Leveraging a suite of such mutation tests establishes a safeguard
against regressions and aids in maintaining a stable and reliable codebase as new features are
developed.
The integration of a watch mode for E2E tests into the development workflow can significantly
improve code quality by enabling developers to identify and resolve integration issues as they code.
It acts as a real-time safeguard against regressions and defects, which is a reflection of high
standards in software development and maintenance.
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: "postgres",
url: "postgres://postgres:root@localhost:5432/test-dev",
synchronize: true,
entities: [Song],
dropSchema: true,
}),
SongModule,
],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
afterEach(async () => {
const songRepository = app.get("SongRepository");
await songRepository.clear();
});
afterAll(async () => {
await app.close();
});
To ensure test accuracy and environment isolation, the Songs table in the database must be cleared
after each test is executed. This practice prevents state leakage between tests and maintains
consistency.
The file to initiate End-to-End testing in watch mode is executed with the command npm run
test:e2e:watch. This initiates a process that continually monitors for any changes in the codebase
and reruns tests accordingly, allowing for immediate feedback and efficient debugging.
expect(results.statusCode).toBe(200);
expect(results.body).toEqual({ data: { songs: [newSong] } });
});
1. Testing queries in Nest.js, especially when involving a database, begins by ensuring that the
relevant records exist to be queried against. The setup phase of a test typically includes
invoking methods like createSong to insert necessary data into the Songs table, resulting in
a controlled and predictable testing environment.
2. The formation of GraphQL queries is facilitated through the use of structured variables, such
as queryData, which hold the GraphQL query string. These queries are then utilized to
interact with the GraphQL API, which, by convention, operates under a single endpoint,
typically /graphql.
3. To execute these queries within a test suite, the query data must be provided to the send
method of the testing tool. This simulates a client request to the server, allowing for the
testing of query handling and response accuracy.
4. In high-quality software engineering, a tip is to ensure the testing environment closely mimics
production conditions without affecting real data, maintaining isolation between tests for
reliability. Additionally, structuring test cases to reflect a variety of realistic scenarios can
uncover edge cases and potential bugs before deployment.
1. Defining a Query in GraphQL with specific operation names and required variables, such as
GetSong($id: ID!), clarifies the expected input and makes the query reusable. Variables
in GraphQL, like $id, are used to parameterize queries, allowing for dynamic input that can
be varied with each query execution.
2. Supplying the $id variable within a song query allows for the specification of which particular
song is being requested. This is a part of query composition in GraphQL, where the query
variables are declared and then used within the query body to fetch data.
3. The variables section in a GraphQL operation is where the actual values for the declared
variables are specified. This is where one sets the specific ID value for $id when executing
the query, thereby allowing for the retrieval of data corresponding to that particular identifier.
expect(results.body.data.createSong.title).toBe("Animals");
});
expect(results.body.data.updateSong.affected).toBe(1);
});
expect(results.body.data.deleteSong.affected).toBe(1);
});
For optimal results, these tests would typically include checks for handling of invalid inputs and edge
cases to ensure resilience. Leveraging a suite of such mutation tests establishes a safeguard
against regressions and aids in maintaining a stable and reliable codebase as new features are
developed.
What is DataLoader?
In GraphQL, a dataloader is a mechanism used to efficiently batch and cache requests made to a
backend data source. It is particularly useful in scenarios where multiple GraphQL queries or
mutations request the same or overlapping data, leading to redundant or redundant data-fetching
operations. The dataloader helps to optimize these scenarios by reducing the number of database or
API calls, resulting in improved performance and reduced latency.
The idea behind a data loader is to collect multiple individual data requests and then fetch the data
in batches, minimizing the number of trips to the data source. It acts as a middleware or utility
between the GraphQL resolver functions and the data source, sitting at the data access layer.
1. Request Batching: When a resolver needs to fetch data from the data source, it registers the
request with the dataloader instead of making the direct call immediately. The dataloader
collects these requests during the GraphQL query execution phase.
2. Batch Execution: Once the execution phase is complete, the dataloader identifies identical or
overlapping requests and groups them into batches. It then executes these batches in one or
more optimized calls to the underlying data source.
3. Caching: The dataloader often incorporates a caching mechanism to store and reuse fetched
data for future requests, further reducing the need for redundant data fetches.
By using a dataloader, GraphQL servers can avoid the "N+1" problem, where resolving N objects in
a GraphQL query requires N+1 database or API calls. Instead, the dataloader can efficiently resolve
multiple objects with a smaller number of data source calls.
Dataloader implementations are available for various programming languages and frameworks that
support GraphQL, making it easier for developers to optimize data fetching in their GraphQL APIs.
For example, in JavaScript, the dataloader library is commonly used for this purpose.
type User {
id: Int!
name: String!
}
type Post {
id: String!
title: String!
body: String!
createdBy: User!
}
type Query {
posts: [Post!]!
users: [User!]!
}
{
posts{
id
title
body
createdBy{
id
name
}
}
}
Upon sending the specified request to the GraphQL server, the system initiates a database query for
the createdBy field within the users table. DataLoader addresses the inefficiency by batching user
IDs and caching them.
Execution of the aforementioned request results in console outputs such as FETCHING DATA FROM
DB: Getting user with id 1... multiple times for the same user ID. This indicates repetitive
fetching of user records for each post by the createdBy resolver. DataLoader mitigates this issue by
batching requests and utilizing caching mechanisms.
The creation of users.loader should take place within the user's directory.
console.log("usersMap", usersMap);
const results = ids.map((id) => usersMap[id]);
console.log("results", results);
return results;
});
}
This specific setup creates a DataLoader that batches user retrieval by their identifiers, leveraging a
service method that fetches users in bulk and a utility function to map the resulting array to an object
for constant-time lookups. This pattern minimizes unnecessary database queries and is a
resource-efficient strategy for fetching associated data, such as user information, by aggregating
multiple queries into a single batch. This approach is reflected in the asynchronous function passed
to the DataLoader constructor, which organizes the users into a map and resolves the batch of user
requests in an optimized sequence that aligns with the initial ordering of IDs.
Step 2: Register Loader in Context
@Module({
imports: [
PostsModule,
GraphQLModule.forRootAsync({
imports: [UsersModule],
useFactory: (usersService: UsersService) => ({
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
context: () => ({
randomValue: Math.random(),
usersLoader: createUsersLoader(usersService),
}),
}),
inject: [UsersService],
}),
],
})
The above configuration within a @Module decorator is setting up a GraphQL module with
asynchronous importation. This approach ensures that dependencies such as UsersModule are
loaded correctly and their services, like UsersService, are available for use within the GraphQL
context.
For efficient data fetching and batching, the usersLoader is instantiated through the
createUsersLoader function, providing a DataLoader instance specifically for user data. This setup
is vital for optimizing GraphQL query performance by reducing the number of data access requests,
particularly beneficial in scenarios with complex data loading requirements such as nested queries.
1. HTTP Requests: It abstracts away the details of making HTTP requests, including handling
GET, POST, PUT, DELETE, etc., so you can focus on fetching data from the API.
2. Caching: It has built-in caching support, which can improve the performance of your
GraphQL server by reducing redundant requests to the same REST API endpoints.
3. Error Handling: It automatically handles errors and HTTP status codes, making it easier to
manage error responses from the REST API.
4. GraphQL Integration: It maps the fetched data to your GraphQL schema, allowing you to
define resolvers that directly use the methods provided by RESTDataSource to fetch data.
We are going to fetch data from the jsonplanceholder api. We will this endpoint
https://jsonplaceholder.typicode.com/todos to fetch all the todos.
type Query {
todos: [Todo!]!
}
Step 2: Implement RESTDatasource in TodoService
import { RESTDataSource } from "@apollo/datasource-rest";
import { Injectable } from "@nestjs/common";
@Injectable()
export class TodoService extends RESTDataSource {
constructor() {
super();
this.baseURL = "https://jsonplaceholder.typicode.com";
}
async getTodos() {
return this.get("/todos");
}
}
Setup Prisma
What is Prisma?
Prisma is different than traditional Object-Relational Mapping (ORM). Unlike traditional ORMs,
Prisma prioritizes the generation of a strongly-typed query API that closely mirrors the database
schema. This approach simplifies database operations, eliminates the need for manual SQL queries,
and enforces robust type safety throughout development. Prisma enjoys popularity in the modern
JavaScript and TypeScript ecosystem and finds extensive use in Node.js applications and
frameworks like Nest.js.
The upcoming endeavor involves constructing REST APIs utilizing Prisma, harnessing its
capabilities to streamline database-related tasks within the API development process.
1. Prisma Client: It functions as a potent query builder and database client, enabling type-safe
interaction with the database using TypeScript or JavaScript. By automatically generating a
query API based on the data model and schema, it delivers typing and autocompletion
support during database queries.
2. Prisma Migrate: This tool empowers you to manage database schema changes in a
versioned and controlled manner. Prisma Migrate simplifies the application of migrations,
ensuring your database schema remains synchronized with your data model.
3. Prisma Studio: This tool provides a visual interface for database management, offering an
intuitive way to inspect and manipulate database data directly from the browser. It facilitates
tasks such as viewing, editing, and exploring data stored in your database.
Prisma supports multiple database connectors, including PostgreSQL, MySQL, SQLite, and SQL
Server, offering compatibility with various database systems.
This will create the prisma folder and schema.prisma file. It will also create the .env file.
# Prisma supports the native connection string format for PostgreSQL, MySQL,
SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options:
https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://postgres:root@localhost:5432/nest-prisma-db?schema=pub
lic"
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
When using Prisma, you define models in a declarative manner using the Prisma schema language.
The Prisma schema language allows you to define the data model of your application, including
entities, fields, data types, relationships, and constraints.
model Song {
id Int @id @default(autoincrement())
title String
}
We have created a song model with id and title. I have set the id to autoincrement automatically
Run Migrations
Migrations in Prisma are a way to manage and apply changes to your database schema in a
version-controlled and systematic manner. They allow you to make changes to your Prisma schema
and then apply those changes to the actual database without losing data or compromising its
integrity.
When you modify your Prisma schema to add, remove, or alter models or fields, you create a new
migration to represent those changes. Migrations are recorded as code files and are stored in the
prisma/migrations directory of your project. Each migration file contains the steps required to
apply or revert a specific change to the database schema.
1. Version Control: Migrations are managed by Prisma and stored as code files. These files
are stored in your version control system (such as Git), enabling collaboration and tracking
changes over time.
2. Idempotent Operations: Migrations are designed to be idempotent, meaning that applying
the same migration multiple times results in the same outcome. This ensures that migrating a
database to a specific state is a repeatable process.
3. Data Preservation: Prisma migrations are designed to be data-safe, meaning that data in
the database is preserved during the migration process. When you alter a table or add new
fields, the existing data is migrated or transformed accordingly.
4. Rollback Support: Migrations also support rollback, which allows you to revert a previously
applied migration and restore the database to its previous state. This feature is useful when
dealing with errors or when undoing specific changes.
To use migrations in Prisma, you need to have Prisma Migrate installed. Prisma Migrate is a
separate component of the Prisma toolkit that provides migration functionality
Let's run the migration using npx prisma migrate dev --name=init
I have provided the name of the migration init. You can provide any name for your migrations
This migration command will generate the database tables for you. You can check the tables from
your PGAdmin. It will also generate the Typescript typings for you with the help of PrismaClient
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
onModuleInit() {
this.$connect();
}
}
You have to create a new Prisma service inside the src folder. This PrismaService will make the
connect with PrismaClient and interact with DB
You have to select the REST API and Endpoint to yes. This command will generate the songs
module, controller, and service for you
create(createSongDto: Prisma.SongCreateInput) {
return this.prisma.song.create({
data: createSongDto,
});
}
findAll() {
return this.prisma.song.findMany(); //find all the records
}
findOne(id: number) {
return this.prisma.song.findUnique({ where: { id } }); // find unique single
record based on id
}
}
We are going to use the types from the Prisma Client package. PrismaClient has generated many
types of us you can find all of the types in node_modules/.prisma/client/index.d.ts
I have used the SongCreateInput for createSongDTO. You can find this type in index.d.ts file
@Post()
create(@Body() createSongDto: Prisma.SongCreateInput) {
return this.songsService.create(createSongDto);
}
{
"title" : "FOLLOW ME"
}
update(
where: Prisma.SongWhereUniqueInput,
updateSongDto: Prisma.SongUpdateInput,
) {
return this.prisma.song.update({
where,
data: updateSongDto,
});
}
I have used the SongWhereUniqueInput. You can find this type from PrismaClient. I have also
used the SongUpdateInput from PrismaClient
//songs.controller.ts
@Delete(':id')
remove(@Param('id') id: string) {
return this.songsService.remove({ id: +id });
}
One to Many Relation
model Artist {
id Int @id @default(autoincrement())
title String
name String
songs Song[]
}
● It uses the @relation attribute to define the relationship. The @relation attribute has two
arguments: fields and references. It specifies that the artist field in the Song model
references the id field in the Artist model.
● artistId: This field is of type Int and represents the foreign key that links a Song to its
corresponding Artist. It is optional (Int?) to allow for songs that may not have an
associated artist.
//artists.controller.ts
@Post()
create(@Body() createArtistDto: Prisma.ArtistCreateInput) {
return this.artistsService.create(createArtistDto);
}
//artists.module.ts
providers: [ArtistsService, PrismaService],
{
"name" : "Avicci"
}
We have used that type in the create method which means I can add artistId in the request body
while creating a new song
You can populate the record of the artist while fetching all the songs. You can include the artist in the
query like this
findAll() {
return this.prisma.song.findMany({ include: { artist: true } });
}
Now you will send the request to fetch all songs and you will get the artist record with a song
model User {
id Int @id @default(autoincrement())
name String
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId Int @unique
photo String
phone String
}
● profile: This field represents the relationship between the User model and the Profile
model. It uses the @relation attribute to define the relationship.
● The @relation attribute has two arguments: fields and references. It specifies that the
profile field in the User model references the id field in the Profile model.
● The ? indicates that the profile field is optional, meaning a user may or may not have an
associated profile
● It uses the @unique attribute to ensure that each Profile is associated with a unique User
● categories: This field represents the relationship between the Post model and the
CategoriesOnPosts model. It is an array of CategoriesOnPosts, indicating that a post can
have multiple categories associated with it.
model Category {
id Int @id @default(autoincrement())
name String
posts CategoriesOnPosts[]
}
● posts: This field represents the relationship between the Category model and the
CategoriesOnPosts model. It is an array of CategoriesOnPosts, indicating that a category
can be associated with multiple posts.
model CategoriesOnPosts {
post Post @relation(fields: [postId], references: [id])
postId Int
category Category @relation(fields: [categoryId], references: [id])
categoryId Int
@@id([postId, categoryId])
}
● post: This field represents the relationship between the CategoriesOnPosts model and the
Post model. It uses the @relation attribute to define the relationship. The @relation
attribute has two arguments: fields and references. It specifies that the post field in the
CategoriesOnPosts model references the id field in the Post model.
● postId: This field is of type Int and represents the foreign key that links a
CategoriesOnPosts to its corresponding Post.
● category: This field represents the relationship between the CategoriesOnPosts model
and the Category model. It uses the @relation attribute to define the relationship. The
@relation attribute has two arguments: fields and references. It specifies that the
category field in the CategoriesOnPosts model references the id field in the Category
model.
● categoryId: This field is of type Int and represents the foreign key that links a
CategoriesOnPosts to its corresponding Category.
● assignedAt: This field represents the timestamp when the category was assigned to the
post. It uses the DateTime type and has a default value of now(), meaning it will be set to
the current date and time when a new record is created.
● assignedBy: This field represents the name of the user who assigned the category to the
post and is of type String.
● @@id([postId, categoryId]): This attribute defines a composite primary key using both
postId and categoryId as the primary key fields. This ensures that a combination of post
and category uniquely identifies each record in the CategoriesOnPosts table, allowing for a
many-to-many relationship between posts and categories.
//posts.service.ts
create(createPostDto: Prisma.PostCreateInput) {
return this.prisma.post.create({ data: createPostDto })
}
Now we need to send the request to create a post with 2 new categories in the DB
{
"title": "One to Many Relation",
"categories": {
"create": [
{
"assignedBy": "Jane",
"asignedAt": "2023-08-01T10:03:38.016Z",
"category": {
"create": {
"name": "Prisma"
}
}
},
{
"assignedBy": "Jane",
"asignedAt": "2023-08-01T10:03:38.016Z",
"category": {
"create": {
"name": "Nest.js"
}
}
}
]
}
}
POST http://localhost:3000/posts
Content-Type: application/json
{
"title": "Transactions in Prisma",
"categories": {
"create": [
{
"assignedBy": "Bob",
"asignedAt": "2023-08-01T10:07:00.918Z",
"category": {
"connect": {
"id": 1
}
}
},
{
"assignedBy": "Bob",
"asignedAt": "2023-08-01T10:07:00.918Z",
"category": {
"connect": {
"id": 2
}
}
}
]
}
}
//posts.controller.ts
@Get()
findAll(
@Body()
where: Prisma.PostWhereUniqueInput,
){
return this.postsService.findAll(where);
}
Let's run the query to fetch all the posts with the Nest.js category
{
"categories": {
"some": {
"category": {
"name": "Nest.js"
}
}
}
}
Nested Queries
What is Transaction
In Prisma, a transaction is a way to group multiple database operations into a single logical unit of
work that must either be fully completed or fully rolled back. Transactions ensure data consistency
and integrity by making sure that if any part of the transaction fails, all changes made within that
transaction are reverted, and the database remains in a consistent state.
● You want to create two or more records related by ID at the same time (for example, create a
blog post and a user)
● You want to update and create records related by ID at the same time (for example, change
a user's name and create a new blog post)
Use Case
● There is a One to one relationship between Customer and Address
● There is a One to many relationship between Customer and Application
model Customer {
id Int @id @default(autoincrement())
name String
email String @unique
address Address? @relation(fields: [addressId], references: [id])
applications Application[]
addressId Int? @unique
}
model Address {
id Int @id @default(autoincrement())
zip String?
city String
country String
Customer Customer?
}
enum APPLICATION_TYPE {
LOAN
CAR_FINANCING
BUSINESS_FINANCING
}
model Application {
id Int @id @default(autoincrement())
type APPLICATION_TYPE @default(LOAN)
tenure String
amount Int
@Post()
create(@Body() createApplicationDto: Prisma.CustomerCreateInput) {
return this.applicationsService.create(createApplicationDto);
}
{
"email": "jane1@gmail.com",
"name": "Jone Doe",
"address": {
"create": {
"city": "New York",
"country": "USA",
"zip": "34443"
}
},
"applications": {
"create": [
{
"amount": 32224,
"tenure": "5 Years",
"type": "BUSINESS_FINANCING"
}
]
}
}
Application is dependent on the customerId and addressId If any of the records fails to be
created then the application will not be created or if any error occurs while creating the application it
will roll back the transaction for the customer and address. The records will be deleted in the
customer and address tables
If you have to perform batch and bulk operations like deleteMany, createMany
We are going to run multiple queries at the same time, if any of query fails you will not get any
results back. It helps when you are creating multiple records at the same time. If one record fails
then the complete operation will be discarded
return this.prisma.$transaction([
this.prisma.post.findMany(),
this.prisma.artist.findMany(),
this.prisma.song.findMany(),
this.prisma.application.findMany(),
]);
Interactive Transactions
Use Case:
● You are going to build the account transfer feature in the banking application
● John wants to transfer the amount of $100 to Sam Account
Process / Flow
1. Update the John Account or deduct the $100 from John
2. Check the balance of John account
3. If balance is less than $0 you have to rollback the transaction. The operation should not be
continue
4. If any of the step fails the complete operation will be discarded
create(createAccountDto: Prisma.AccountCreateInput) {
return this.prisma.account.create({ data: createAccountDto });
}
{
"title" : "John",
"balance" : 100
}
{
"title" : "Sam",
"balance" : 100
}
transfer(transferAccountDTO: TransferAccountDTO) {
throw new Error('Method not implemented.');
}
Step 5: Implement Account Transfer Logic
transfer(transferAccountDTO: TransferAccountDTO) {
const { sender: from, receiver: to, amount } = transferAccountDTO;
return this.prisma.$transaction(async (tx) => {
// John Account
// 1. Decrement amount from the sender.
const sender = await tx.account.update({
data: {
balance: {
decrement: amount,
},
},
where: {
id: from,
},
});
return recipient;
});
}
{
"sender" :1,
"receiver" :2,
"amount": 40
}
{
"sender" :1,
"receiver" :2,
"amount": 20
}
You will get an error at the last HTTP operation because John will not have a balance more than $20
so the transaction will be rollback
File Upload
This is a package that provides TypeScript type definitions for the multer package, which is a
middleware for handling multipart/form-data (typically used for uploading files) in Node.js.
When working with TypeScript, it's important to have accurate type definitions for third-party libraries
like multer. These type definitions help you write more reliable and type-safe code by providing
information about the functions, objects, and properties that the library exposes.
import {
HttpStatus,
ParseFilePipeBuilder,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
console.log(file);
}
● The interceptor is configured with a diskStorage option, specifying that the uploaded files
will be stored on the local disk. The destination option specifies the directory where the
uploaded files will be saved, and the filename option determines the name of the saved file
(in this case, it retains the original name)
● ParseFilePipeBuilder: This is a custom utility that creates a pipe for validating uploaded
files. In this code, it's used to add a validation check to ensure that the uploaded file is of
type PNG
Custom Decorator
@Get('/user/:id')
findOne(
@User()
user: UserEntity,
) {
console.log(user);
return user;
}
imports: [ScheduleModule.forRoot()],
@Injectable()
export class TaskService {
private readonly logger = new Logger(TaskService.name);
@Cron("0 * * * * *")
myCronTask() {
this.logger.debug("Cron Task Called");
}
}
* * * * * *
| | | | | |
| | | | | day of week
| | | | months
| | | day of month
| | hours
| minutes
seconds (optional)
Examples
* * * * * * every second
45 * * * * * every minute, on the 45th second
0 10 * * * * every hour, at the start of the 10th minute
0 */30 9-17 * * * every 30 minutes between 9am and 5pm
0 30 11 * * 1-5 Monday to Friday at 11:30am
Cookies
1. Session Management: Cookies are often used to manage user sessions. When a user logs
in to a website, a session cookie is created, which allows the server to recognize the user
and maintain their authenticated state as they navigate different pages on the site.
2. Personalization: Cookies can store user preferences and settings, allowing websites to
customize the user experience based on their previous interactions. This could include
remembering language preferences, display settings, and more.
3. Shopping Carts and E-Commerce: E-commerce websites use cookies to maintain
shopping carts. Items added to a cart are stored in a cookie, allowing the user to continue
shopping and complete their purchase later.
4. Tracking and Analytics: Cookies are often used to gather data about user behavior, such
as which pages they visit, how long they stay on a page, and what actions they take. This
data is then used for analytics and to improve the website's performance and user
experience.
5. Authentication and Security: Cookies can help with authentication and security by storing
tokens or other information that verifies a user's identity. For example, a website might use
cookies to remember that a user is logged in, so they don't have to log in again on every
page.
6. Remember Me Functionality: Many websites offer a "Remember Me" option when logging
in. This sets a persistent cookie that allows the user to stay logged in even after closing and
reopening their browser.
7. Tracking User Activity: Cookies can be used to track a user's activity across different
websites. This is often used for targeted advertising and marketing.
8. Load Balancing: In some cases, cookies are used to help distribute website traffic across
multiple servers, ensuring a more even load distribution.
9. State Management: Cookies are often used to manage state information in web
applications. For example, they can be used to remember where a user was in a multi-step
process.
10. Caching: Cookies can be used to cache certain information on the user's device, reducing
the need to repeatedly request the same data from the server.
While cookies offer many benefits, it's important to note that there are also concerns related to
privacy and security. Some users may disable or delete cookies to protect their privacy, and there
are regulations in place, such as the General Data Protection Regulation (GDPR), that govern how
cookies and user data can be used.
In recent years, there has been an increased focus on alternative solutions like server-side sessions,
local storage, and newer web technologies like Web Storage API and IndexedDB. These options
provide alternatives to traditional cookies for storing data on the user's device.
app.use(cookieParser());
@Get('set-cookie')
setCookie(
@Res({ passthrough: true })
response: Response,
) {
response.cookie('Cookie token Name', 'encrypted cookie string');
response.send('Cookie Saved Successfully');
}
@Get('get-cookie')
finndAll(@Req() req: Request) {
console.log(req.cookies);
return req.cookies;
}
Step 4: Test
When you run the application you have to send the setCookies request first from your browser. The
cookie will be saved in your Google Chrome Browser. You have to open chrome dev tools and find
the Application tab. You can find your cookies there.
Queues
A common use case for queues is to process tasks like sending emails, generating reports,
processing images, or any other task that can be offloaded from the main application flow.
1. Task Generation: In your application, you identify tasks that can be processed
asynchronously. For example, let's consider sending an email after a user registers. Instead
of sending the email directly within the registration endpoint, you can push the task of
sending the email to a queue.
2. Queue Library Integration: You integrate a queue library of your choice (e.g., Bull) into your
Nest.js application. This involves installing the library, configuring it, and creating a queue
instance.
3. Enqueuing Tasks: When you want to perform a task asynchronously, you enqueue it in the
queue. For our example, after a user registers, you enqueue a task to send the registration
email.
4. Worker Process: You create one or more worker processes that continuously monitor the
queue for tasks. When a task is available in the queue, a worker picks it up and processes it.
In our example, the worker would send the registration email.
5. Background Execution: The task is executed in the background, separate from the main
application thread. This ensures that the main application remains responsive and doesn't
get blocked by time-consuming tasks.
version: "3"
services:
redis:
image: redis:alpine
ports:
- 6379:6379
Let's start the redis service by opening the terminal and run docker-compose up
@Controller("audio")
export class AudioController {
constructor(
@InjectQueue("audio-queue")
private readonly audioQueue: Queue
) {}
/**
* Let's imagine we would like to convert .wav file into .mp3
*/
@Post("convert")
async convert() {
await this.audioQueue.add("convert", {
file: "sample.wav",
});
}
}
@Processor("audio-queue")
export class AudioProcessor {
private logger = new Logger(AudioProcessor.name);
@Process("convert")
handleConvert(job: Job) {
this.logger.debug("start converting wav file to mp3");
this.logger.debug(job.data);
this.logger.debug("file converted successfully");
}
}
● @Processor('audio-queue'): This decorator specifies that this class is a processor for the
"audio-queue". This means that it will handle tasks enqueued in the "audio-queue".
● @Process('convert'): This decorator specifies that the handleConvert method should
handle tasks with the name "convert". When a task with the name "convert" is enqueued in
the "audio-queue", the handleConvert method will be triggered to process it.
● handleConvert(job: Job): This is the method that processes tasks with the name
"convert". It takes a Job object as its parameter, which contains information about the task
and its data
Event Emitter
An event emitter works on the principle of publishers and subscribers. The entity that generates an
event is called the "publisher," and the entity that listens and responds to the event is called the
"subscriber."
Use Case
● We are going to take an example from our previous example let's say we want to send the
notification to the user when the .wav file converts successfully into .mp3 format.
● You can use EventEmitter to send the notification to the user
@Injectable()
export class AudioConvertedListener {
@OnEvent('audio.converted') // We have registered a new event lister with
audio.converted name
handleAudioConvertedEvent(event: AudioConvertedEvent) { //We have to create the
type for the AudioConvertedEvent
console.log(event);
// Here you can have your EmailService method you can call here
console.log(
'Notification has been sent to user that file is converted successfully,
);
}
}
Make sure you have injected the EventEmitter dependency in your AudioProcessor class
handleConvert(job: Job){
//...
this.eventEmitter.emit('audio.converted', job.data);
}
Streaming
What is Streaming
Streaming in Nest.js refers to the process of sending or receiving data in small chunks, called
"streams," rather than sending or receiving the entire data at once. This concept is based on the
Stream API in Node.js and is utilized for more efficient data handling, especially when dealing with
large amounts of data, such as files, network requests, or real-time data transmission.
@Get('stream-file-customize')
getFileCustomizedResponse(@Res({ passthrough: true }) res): StreamableFile {
const file = createReadStream(join(process.cwd(), 'package.json'));
res.set({
'Content-Type': 'application/json',
'Content-Disposition': 'attachment; filename="package.json',
});
return new StreamableFile(file);
}
Session
What is a session?
In Nest.js, sessions refer to the concept of maintaining stateful data between consecutive requests
from the same client. A session allows you to store and retrieve user-specific information on the
server across multiple HTTP requests, typically using cookies to identify the session.