NestJS is an MVC framework for building efficient, scalable Node.js server-side applications.
It is built with and fully supports TypeScript (yet still enables developers to code in pure JavaScript). It also combines elements of Object Oriented Programming, Functional Programming, and Functional Reactive Programming.
One of the key benefits of Nest is that it provides an out-of-the-box application architecture that allows developers and teams to create highly testable, scalable, loosely coupled, and easily maintainable applications.
What we are building
In this post, I will take you through the journey of getting started with Nest. We will build a Mini Blog that’s a Web RESTful API Application.
This simple Mini Blog application will cover:
- Setting up Sequelize and Postgres Database
- Authentication with Passport (Login and Sign up)
- Validating user input
- Route protection with JWT
- Creating, Reading, Updating, and Deleting a blog post
Prerequisites
Knowledge of TypeScript and JavaScript is very important to follow along with this tutorial. Experience with Angular is a plus, but no worries – this post will explain every concept you need to know about Nest.
You will need to install Postman, as we will use it to test our API endpoints. And also make sure you have Node.js (>= 8.9.0) installed on your machine. Lastly, you can find a link to the final project GitHub repo here.
Building blocks
Before we get started, we’ll discuss some abstractions/concepts that will help you know where to put specific business logic from project to project.
Nest is very similar to Angular – so if you are familiar with Angular concepts it will be straightforward to you.
Still, I’ll assume that you have no knowledge of these concepts and will explain them to you.
Controller
The controller is responsible for listening to requests that come into your application. It then formulates the responses that go out.
For instance, when you make an API call to /posts
the controller will handle this request and return the appropriate response you specified.
This is just a basic Class declaration in TypeScript/JavaScript with a @Controller
decorator. All Nest Controllers must have the decorator which is required to define a basic Controller in Nest.
Nest allows you to specify your routes as a parameter in the @Controller()
decorator. This helps you group a set of related routes and minimises code repetition. Any request to /posts
will be handled by this controller.
At the class methods level, you can specify which method should handle the GET
, POST,
DELETE
, PUT/PATCH
HTTP requests.
In our example, the findAll()
method with the @Get()
decorator handles all GET
HTTP requests to get all blog posts. While the findOne()
method with the @Get(': id')
decorator will handle a GET /posts/1
request.
Providers
Providers were designed to abstract any form of complexity and logic to a separate class. A provider can be a service, a repository, a factory, or a helper.
Providers are plain TypeScript/JavaScript classes with an @Injectable()
decorator preceding their class declaration. Just like services in Angular, you can create and inject providers into other controllers or other providers as well.
A good use case for a service provider is to create a PostService that abstracts all communication to the database into this service. This keeps the PostsController
nice and clean.
This is just a plain TypeScript class with a @Injectable()
decorator (this is how Nest knows it is a provider). Post
is just an interface for type checking.
Here, we are using a simple data structure to store the data. In a real project, this service will be communicating with the database.
Modules
A module is a JavaScript/TypeScript class with the @Module()
decorator.
The @Module()
decorator provides metadata that Nest uses to organise the application structure.
Modules are a very important aspect of Nest and each application must provide at least one Module: the application root module. The root module is the starting point Nest uses to build the application graph.
The post service, controller, post entity, and everything related to post should be grouped into a module (PostsModule). Below, we have defined the PostsModule.
Then, we import this module into the root module AppModule
:
The @Module()
decorator takes a single object whose properties describes the module:
imports:
Other modules that are needed by this module.exports:
By default, modules encapsulate providers. It’s impossible to inject providers that are neither directly part of the current module nor are exported from the imported modules. To make the current module providers available to other modules in the application, they have to be exported here. We can also export modules we imported too.controllers:
The set of controllers defined in this module which have to be instantiated.providers:
in simple terms, all our services and providers within the module will be here.
Interceptor
An interceptor is a specialised set of middleware that lets you peek into the request that goes into the application. You can peek into the request either before it reaches the controller or after the controller is done with the request before it gets to the client-side as a response. You can manipulate the data on their way out in the interceptor.
Guard
Guard is also a special kind of middleware that is used mainly for authentication and authorisation. It only returns a boolean value of true or false.
Guards have a single responsibility: they determine whether a given request will be handled by the route handler or not, depending on certain conditions (like permissions, roles, ACLs, etc.) present at run-time.
A Guard should also implement the CanActivate
interface.
Pipe
Pipes are also a special kind of middleware that sits between the client and the controller. They are mostly used for validation and transforming data before they get to the controller.
DTO (Data Transfer Object)
Data transfer object is an object that defines how data will be sent over the network. They are also used for validation and type checking.
Interfaces
TypeScript interfaces are only used for type-checking and they do not compile down into JavaScript code.
Installation
Install the NestJs CLI. Nest comes with an awesome CLI that makes it easy to scaffold a Nest application with ease. In your terminal or cmd run:
npm i -g @nestjs/cli
Now you have Nest installed globally in your machine.
On your terminal or cmd, cd into the directory where you want to create your application, and run following commands:
nest new nest-blog-api
cd nest-blog-api
npm run start:dev
Navigate to http://localhost:3000
on any of your browsers. You should see Hello World
. Bravo! you have created your first Nest app. Let’s continue.
NOTE: As of this writing, if running npm run start:dev
throws an error, change your typescript:3.4.2
in your package.json file
to typescript:3.7.2
and then delete the node_modules and package-lock.json
re-run npm i
.
Your folder structure should look like this:
Sequelize and Database Setup
We’ll start by installing the following dependencies. Make sure your terminal or cmd is currently on your project root directory. Then run the following commands:
npm install -g sequelize
npm install --save sequelize sequelize-typescript pg-hstore pg
npm install --save-dev @types/sequelize
npm install dotenv --save
Now, create a database module. Run nest generate module /core/database
.
Database Interface
Inside the database folder, create an interfaces
folder, then create a dbConfig.interface.ts
file inside it. This is for the database configuration interface.
Each of the database environments should optionally have the following properties. Copy and paste the following code:
Database Configuration
Now, let’s create a database environment configuration. Inside the database folder, create a database.config.ts
file. Copy and paste the below code:
The environment will determine which configuration should be used.
.env file
On our project root folder, create .env
and .env.sample
files. Copy and paste the following code into both files:
Fill the values with the correct information – only on the .env
file – and make sure it’s added to the .gitignore
file to avoid pushing it online. The .env.sample
is for those who want to download your project and use it so you can push it online.
HINTS: Your username, password, and database name should be what you use to set up your Postgres. Create a Postgres database with your database name.
Nest provides a @nestjs/config
package out-of-the-box to help load our .env
file. To use it, we first install the required dependency.
Run npm i --save @nestjs/config
.
Import the @nestjs/config
into your app root module:
Setting the ConfigModule.forRoot({ isGlobal: true })
to isGlobal: true
will make the .env
properties available throughout the application.
Database Provider
Let’s create a database provider. Inside the database folder, create a file called database.providers.ts
.
The core directory will contain all our core setups, configuration, shared modules, pipes, guards, and middlewares.
In the database.providers.ts
file, copy and paste this code:
Here, the application decides what environment we are currently running on and then chooses the environment configuration.
All our models will be added to the sequelize.addModels([User, Post])
function. Currently, there are no models.
Best practice: It is a good idea to keep all string values in a constant file and export it to avoid misspelling those values. You’ll also have a single place to change things.
Inside the core folder, create a constants
folder and inside it create an index.ts
file. Paste the following code:
Let’s add the database provider to our database module. Copy and paste this code:
We exported the database provider exports: [...databaseProviders]
to make it accessible to the rest of the application that needs it.
Now, let’s import the database module into our app root module to make it available to all our services.
Setting a global endpoint prefix
We might want all our API endpoints to start with api/v1
for different versioning. We don’t want to have to add this prefix to all our controllers. Fortunately, Nest provides a way to set a global prefix.
In the main.ts
file, add app.setGlobalPrefix('api/v1');
User Module
Let’s add a User module to handle all user-related operations and to keep tabs on who is creating what post.
Run nest generate module /modules/users
.
This will automatically add this module to our root module AppModule
.
Generate User Service
Run nest generate service /modules/users
.
This will automatically add this service to the Users module.
Set Up User Database Schema Model
Inside modules/users
, create a file called user.entity.ts
then copy and paste this code:
Here, we are specifying what our User table will contain. The @column() decorator
provides information about each column in the table. The User table will have name
email
password
and gender
as columns. We imported all the Sequelize decorators from sequelize-typescript
. To read more about Sequelize and TypeScript, check this out.
User DTO
Let’s create our User DTO (Data Transfer Object) schema. Inside the users folder, create a dto
folder. Then create a user.dto.ts
file inside it. Paste the following code in:
User Repository provider
Now, create a User Repository provider. Inside the user’s folder, create a users.providers.ts
file. This provider is used to communicate with the database.
Add this export const USER_REPOSITORY = 'USER_REPOSITORY';
to the constants index.ts
file.
Also, add the user provider to the User module. Notice, we added the UserService to our exports
array. That is because we’ll need it outside of the User Module.
Let’s encapsulate user operations inside the UsersService. Copy and paste the following code:
Here, we injected the user repository to communicate with the DB.
create(user: UserDto)
This method creates a new user into the user table and returns the newly created user object.findOneByEmail(email: string)
This method is used to look up a user from the user table by email and returns the user.findOneById(id: number)
This method is used to look up a user from the user table by the user Id and returns the user.
We will use these methods later.
Lastly, let’s add the User model to the database.providers.ts
file sequelize.addModels([User]);
.
Auth Module
Generate Auth Module
This module will handle user authentication (Login and Sign up).
Run nest generate module /modules/auth
.
This will automatically add this module to our root module AppModule
Generate Auth Service
Run nest generate service /modules/auth
.
This will automatically add this service to the Auth module.
Generate Auth Controller
Run nest g co /modules/auth
.
This will automatically add this controller to the Auth module.
Note: g
is an alias for generate
and co
is for controller
.
We will be using Passport to handle our authentication. It is straightforward to integrate this library with a Nest application using the @nestjs/passport module.
We will implement two auth strategies for this application:
- Local Passport Strategy: This strategy will be used for logging in users. It will verify if the email/username and password provided by the user is valid or not. If user credentials are valid, it will return a token and user object, if not, it will throw an exception.
- JWT Passport Strategy: This strategy will be used to protect protected resources. Only authenticated users with a valid token will be able to access these resources or endpoints.
Local Passport Strategy
Runnpm install --save @nestjs/passport passport passport-local
npm install --save-dev @types/passport-local
npm install bcrypt --save
Inside the auth folder create a local.strategy.ts
file and add the following code:
Here, we are importing Strategy, PassportStrategy and AuthService.
We extend the PassportStrategy
to create the LocalStrategy.
In our use case with passport-local, there are no configuration options, so our constructor simply calls super()
without any options object.
We must implement the validate()
method. For the local-strategy, Passport expects a validate()
method with the following signature: validate(username: string, password:string): any
.
Most of the validation work is done in our AuthService
(with the help of our UserService
), so this method is quite straightforward.
We call the validateUser()
method in the AuthService
(we are yet to write this method), which checks if the user exists and if the password is correct. authService.validateUser()
returns null if not valid or the user object if valid.
If a user is found and the credentials are valid, the user is returned so Passport can complete its tasks (e.g., creating the user
property on the Request
object), and the request handling pipeline can continue. If it’s not found, we throw an exception and let our exceptions layer handle it.
Now, add the PassportModule, UserModule
and LocalStrategy
to our AuthModule.
AuthService
Let’s implement the validateUser()
method.
Here, we check if the user exists with the email provided. Then we check if the password in the DB matched what the User provided. If any of these checks fail, we return null,
if not, we return the user object.
comparePassword(enteredPassword, dbPassword):
This private method compares the user-entered password and user DB password and returns a boolean. If the password matches it returns true. If not, it returns false.
JWT Passport Strategy
Runnpm install @nestjs/jwt passport-jwt
npm install @types/passport-jwt --save-dev
Inside the auth folder create a jwt.strategy.ts
file and add the following code:
Here, we are extending PassportStrategy.
Inside the super()
we added some options object. In our case, these options are:
jwtFromRequest:
supplies the method by which the JWT will be extracted from theRequest
. We will use the standard approach of supplying a bearer token in the Authorization header of our API requests.ignoreExpiration
: just to be explicit, we choose the defaultfalse
setting, which delegates the responsibility of ensuring that a JWT has not expired to the Passport module. This means that if our route is supplied with an expired JWT, the request will be denied and a401 Unauthorized
response sent. Passport conveniently handles this automatically for us.secretOrKey
: This is our secret key for the token. This will use the secret key in our.env
file.- The
validate(payload: any)
For the jwt-strategy, Passport first verifies the JWT’s signature and decodes the JSON. It then invokes ourvalidate()
method passing the decoded JSON as its single parameter. Based on the way JWT signing works, we’re guaranteed that we’re receiving a valid token that we have previously signed and issued to a valid user. We confirm if the user exists with the user payload id. If the user exists, we return the user object, and Passport will attach it as a property on theRequest
object. If the user doesn’t exist, we throw an Exception.
Now, add the JwtStrategy
and JwtModule
to the AuthModule.
:
We configure the JwtModule
using register()
, passing in a configuration object.
Let’s add other methods we will need to login and create a new user in AuthService
:
Import and inject JwtService.
login(user):
This method is used to login the user. This takes the user information, generates a token with it, and then returns the token and user object.create(user):
This method is used to create a new user. This takes the user information, hash the user password, saves the user to the DB, removes the password from the newly returned user, generates a token with the user object, and then returns the token and user object.generateToken(user):
This private method generates a token and then returns it.hashPassword(password):
This private method hashes the user password and returns the hashed password.
We will be using all these functions later.
AuthController
Now, let’s create our signup
and login
methods:
When we hit this endpoint POST api/v1/auth/login
will call @UseGuards(AuthGuard('local'))
. This will take the user email/username and password, then run the validate method on our local strategy class. The login(@Request() req)
will generate a JWT token and return it.
The POST api/v1/auth/signup
endpoint will call the this.authService.create(user)
method, create the user, and return a JWT token.
Let’s try it out…
Open your Postman application and make sure it’s running. Send a POST request to http://localhost:3000/api/v1/auth/signup
and input your body data to create a user. You should get a token and the user object returned.
Now that we have a user, let’s log the user in. Send a POST request to http://localhost:3000/api/v1/auth/login
and input just your username and password. You should get a token and the user object returned.
Validation
Notice how we are not validating any of the user’s input. Now, let’s add validation to our application.
Run npm i class-validator class-transformer --save
.
Inside the core folder, create a pipes folder and then create validate.pipe.ts
file. Copy and paste the following code:
Let’s auto-validate all our endpoints with dto
by binding ValidateInputPipe
at the application level. Inside the main.ts
file, add this:
Now, let’s update our users dto
file:
Here, we are importing these decorators from class-validator.
@IsNotEmpty():
ensures the field isn’t empty.@IsEmail():
checks if the email entered is a valid email address.@MinLength(6):
ensures the password character is not less than six.@IsEnum:
ensures only the specified value is allowed (in this case, male and female).
class-validator has tons of validation decorators – check them out.
Let’s try our validation out…
Without passing any value, I got the following validation error. Our validation is working now. This validation is automatic to all endpoints with a dto (data transfer object).
Unique User account
Let’s add a guard that prevents users from signing up with the same email twice since email is unique at the schema level.
Inside the core folder, create a guards folder, then create a doesUserExist.guard.ts
file. Copy and paste the following code:
Now, let’s add this guard to our signup method in AuthController.
:
Let’s try to create a user with an email that already exists in our database:
Post Module
Run nest g module /modules/posts
.
This will automatically add this module to our root module AppModule
.
Generate Post Service
Runnest g service /modules/posts
.
This will automatically add this service to the Post module.
Generate Post Controller
Run nest g co /modules/posts
,
This will automatically add this controller to the Post module.
Post Entity
Create a post.entity.ts
file inside the posts folder. Copy and paste the following code:
The only new thing here is the @ForeignKey(() => User)
specifying that the userId column is the id of the User table and @BelongsTo(() => User)
specifying the relationship between the Post table and User table.
Post DTO (Data Transfer Object)
Inside the posts folder, create a dto
folder then create a post.dto.ts
file inside it. Copy and paste the following code:
Here, our post body object must have a title, and body and title length must not be less than 4.
Post Provider
Create a posts.providers.ts
file inside the posts folder. Copy and paste the following code:
Add this export const POST_REPOSITORY = 'POST_REPOSITORY';
to the constants index.ts
file.
Add our Post provider to our Post Module file:
Now, add our Post entity to our database provider. Import the Post entity inside the database.providers.ts
file, add the Post to this method:
sequelize.addModels([User, Post]);
Post Service Methods
Copy and paste the following inside the Post service file:
Here, we are injecting our Post repository to communicate with our database.
create(post: PostDto, userId):
This accepts post object and the id of the user creating the post. It adds the post to the database and returns the newly created Post. ThePostDto
is for validation.findAll():
This gets all the posts from the database and also includes/eager load the user who created it while excluding the user password.findOne(id):
This finds and returns the post with the id. It also includes/eager load the user who created it while excluding the user password.delete(id, userId):
This deletes the post from the database with the id and userId. Only the user who created the post can delete it. This returns the number of rows that were affected.update(id, data, userId):
This updates an existing post whereid
is the id of the post,data
is the data to update,userId
is the id of the original creator. This returns the number of rows that were updated and the newly updated object.
Post Controller Methods
Copy and paste the following inside the Post controller file:
Most of the CRUD operation functionality is done in our PostService.
findAll():
This handlesGET
request toapi/v1/posts
endpoint. It returns all the posts in our database.findOne(@Param(‘id’) id: number):
This handlesGET
request toapi/v1/posts/1
endpoint to get a single post, where 1 is the id of the post. This throws a 404 error if it doesn’t find the post and returns the post object if it does find the post.create(@Body() post: PostDto, @Request() req):
This handlesPOST
request toapi/v1/posts
endpoint to create a new post.@UseGuards(AuthGuard(‘jwt’))
is used to protect the route (remember our JWT strategy). Only logged in users can create a post.update(@Param(‘id’) id: number, @Body() post: PostDto, @Request() req):
This handles thePUT
request toapi/v1/posts
endpoint to update an existing post. It is also a protected route. If thenumberOfAffectedRows
is zero that means no post with the params id was found.remove(@Param(‘id’) id: number, @Request() req):
This handles theDELETE
request to delete an existing post.
Let’s try our CRUD operation out…
Create a Post
Log in and add your token since creating a post route is a protected route.
Read a single Post
This route isn’t protected, so it can be accessed without the token.
Reading all Posts
This route isn’t protected, so it can be accessed without the token too.
Updating a Single Post
This route is protected, so we need a token and only the creator can update it.
Deleting a Post
This route is protected, so we need a token and only the creator can delete it.
Conclusion
Nest.js gives you a more structured way of building your server-side applications with Node.
For more information, check out the official NestJS website here.
Finally, I hope this article was useful to you! The link to the final project GitHub repo is here.