Building a secure and quality NodeJS REST API with NestJS

I am a self-taught and agile software developer with experience in building reliable software solutions for businesses and individuals.
Search for a command to run...

I am a self-taught and agile software developer with experience in building reliable software solutions for businesses and individuals.
No comments yet. Be the first to comment.
In this series, I will explore all about building great GraphQl and REST APIs.
Exploring a simple way to deploy a NestJS API on AWS Elastic Beanstalk while implementing CI/CD with AWS CodePipeline
Besides being secure, a quality API must be: well structured, maintainable, performant, documented, and scalable.
Although many resources talk about the above, it’s hard to find them well aligned together so the developer who’s learning can see a clear big picture of how to properly implement them.
In this article, I will share the blueprints for building a REST API for a project where security and good structure are essential.
This tutorial assumes you know the basics of programming and have experience in using:
The following traits make NestJS one of the best development frameworks for NodeJS backend applications:
Note: This tutorial is just a small guide to building secure REST APIs. Although I’ll try my best to be as explicit as possible, you should read more about NestJS and other technologies used here to understand them better.
Since this is a hands-on tutorial we’re gonna learn while building a super-simple accounting app where a user can sign up, log in, manage transactions and log out.
Here’s what our data model will look like:

A demo of what we’re about to build can be found here and the code for this project is entirely available on this GitHub repository.
First, make sure you have the Nest CLI installed globally. Here’s how to install it:
npm i -g @nestjs/cli
Now, let’s set up a new NestJS project:
nest new accounting-app-api
Note: If you get an error of installing dependencies while running the above command, just run
cd accounting-app-api && npm installto move into the project folder and install dependencies.
Once the app is generated by the Nest CLI, do the following to install the dependencies of the project:
npm install --save @nestjs/typeorm typeorm pg @nestjs/swagger @nestjs/config @nestjs/passport @nestjs/jwt passport passport-jwt bcrypt helmet express-rate-limit dotenv joi class-validator class-transformer
npm install --save-dev @types/passport-jwt @types/passport-jwt @types/bcrypt
A brief summary of the technologies used:
pg adapter allows us to interact with our PostgreSQL database.At this stage, the project folder should look like this:
accounting-app-api
├─ src
│ ├─ app.controller.spec.ts
│ ├─ app.controller.ts
│ ├─ app.module.ts
│ ├─ app.service.ts
│ ├─ main.ts
├─ test
│ ├─ app.e2e-spec.ts
│ ├─ jest.e2e.json
└─ .eslintrc.js
├─ .gitignore
├─ .prettierrc
├─ nest-cli.json
├─ package.json
├─ package-lock.json
├─ README.md
├─ tsconfig.build.json
├─ tsconfig.json
We’ll be putting most of our configuration in a .env file. So create a file of name .env in your project directory and paste the following content into it:
# Create a .env file like this one but with correct values for each variable
# -- General
NODE_ENV="development"
PORT=3000
DEBUG=true
# -- Database
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=your_db_password
DB_DATABASE=accounting-app
DB_SSL=false
# The database SSL should be set to true on Heroku
# -- JWT
JWT_SECRET=YourJwtSecretHere
# -- Admin Token
ADMIN_ACCESS_TOKEN=YourSecureAdminAccessToken1234567
The values should be replaced with the correct ones for each of the environment variables.
Since environment variables are not supposed to be exposed, make sure you add the.env file to your .gitignore file.
Next, run the following command in your project’s folder:
nest g module database
The above command will generate the DatabaseModule and include it in your app.module.ts file. You may, then, open the generated module file at src/database/database.module.ts and add the following database configuration to it:
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
autoLoadEntities: true,
synchronize: false,
...(configService.get('DB_SSL') ? {
ssl: true,
extra: {
ssl: {
rejectUnauthorized: false
}
},
} : {})
}),
}),
],
})
export class DatabaseModule {}
You may now set up the configuration module, such that it loads the content in the .env file, merges it with the externally defined environment variables, validates, and avail it through the configuration service by pasting the following code in your app.module.ts:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './database/database.module';
import * as Joi from 'joi';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
validationSchema: Joi.object({
PORT: Joi.number().default(4000),
NODE_ENV: Joi.string().default('development'),
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().required(),
DB_USERNAME: Joi.string().required(),
DB_PASSWORD: Joi.string().required(),
DB_DATABASE: Joi.string().required(),
DB_SSL: Joi.boolean().required(),
}),
}),
DatabaseModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
At this point, the nest app should be able to connect to the database.
Constants & Exceptions
Up to now, we’ve been focusing on environment variables but the API will also need constants such as the app’s name, set up somewhere in the app.
For this, create a folder named common in your src folder and place two files in it: constants.ts and exceptions.ts .
Place the following in your constants.ts file:
export const APP_NAME = "Accounting App REST API";
export const APP_DESCRIPTION = "Simple accounting API.";
export const APP_VERSION = "1.0.0";
export const PASSWORD_HASH_SALT: number = 10;
export const JWT_EXPIRES_IN = '7d';
And the following in your exceptions.ts file:
export const E_USER_EMAIL_TAKEN = "Oops! This email is already taken!";
export const E_USER_NOT_FOUND = "User not found!";
export const E_INCORRECT_EMAIL_OR_PASSWORD = "Email or password entered is incorrect!";
export const E_PASSWORD_INCORRECT = "The password entered is incorrect!";
export const E_TOO_MANY_REQUESTS = "Too many requests!";
export const E_TRANSACTION_NOT_FOUND = "User not found! Please make sure the transaction ID entered is valid!";
export const E_UNAUTHORIZED_ACCESS_TO_RESOURCE = "You are now allowed to access this resource!";
Migrations are files that contain SQL queries for updating a database schema or applying new changes to an existing database.
TypeORM allows you to synchronize the database schema with the entities by setting synchronize: true in the database module configuration object. This is not recommended in production though, because they might cause unwanted data loss or transformation; Which is why, it’s better to set up migrations.
For this, we’re going to create a new TypeORM database configuration file datasource.ts which we shall place inside the src/database and we shall later reference it in our package.json scripts so that the TypeORM CLI, which we’ll use to run migrations, can access our database. After creating the datasource.ts file, paste the following code in it:
Then, add the following scripts to your project’s package.json:
"migration:create": "ts-node ./node_modules/typeorm/cli.js migration:create",
"migration:generate": "ts-node ./node_modules/typeorm/cli.js migration:generate --dataSource src/database/datasource.ts",
"migration:run": "ts-node ./node_modules/typeorm/cli.js migration:run -d src/database/datasource.ts",
"migration:revert": "ts-node ./node_modules/typeorm/cli.js migration:revert -d src/database/datasource.ts",
For the sake of simplicity, just make sure you do the following regarding migrations:
npm run migration:generate src/database/migrations/NameOfTheChange . Make sure you replace NameOfTheChange with a name referring to the change you’ve actually made in your entities. Eg: CreateUsersTable.npm run build before running migrations. Use the following command to run migrations: npm run migration:runThe above commands just allow you to work faster but you can always manually write your own migrations. To do so, first create an empty migration by running npm run migration:create src/database/migrations/NameOfTheChange , then open the migration generated at src/database/migrations/.
Inside every migration file there are two functions:
In order to mitigate or protect our API against certain security exploits, we’re going to implement helmet package, enable CORS, and rate limiting.
For this, copy the code below in your main.ts file.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import helmet from 'helmet';
import { rateLimit } from 'express-rate-limit';
import 'dotenv/config'
import { ValidationPipe } from '@nestjs/common';
import { E_TOO_MANY_REQUESTS } from './common/exceptions';
async function bootstrap() {
// -- App Instantiation
const app = await NestFactory.create(AppModule);
// -- Helmet
app.use(helmet());
// -- Cors setup
app.enableCors({
origin: false, // Specify the allowed origins. I'm setting false to allow requests from any origin
// Find more configuration options here: https://github.com/expressjs/cors#configuration-options
});
// -- Rate limiting: Limits the number of requests from the same IP in a period of time.
// -- More at: https://www.npmjs.com/package/express-rate-limit
app.use(rateLimit({
windowMs: 10 * 60 * 1000, // 10 minutes
max: 100, // Limit each IP to 100 requests per `window` (here, per 10 minutes)
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers,
skipSuccessfulRequests: false, // The counting will skip all successful requests and just count the errors. Instead of removing rate-limiting, it's better to set this to true to limit the number of times a request fails. Can help prevent against brute-force attacks
message: { "message": E_TOO_MANY_REQUESTS, "statusCode": 403, }
}));
// -- Validation
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
}));
// -- Start listening
await app.listen(process.env.PORT ? parseInt(process.env.PORT) : 3000);
}
bootstrap();
Note: Nest has its own rate limiter called @nestjs/throttler which is actually richer and easier to use. I chose the express-rate-limit package because @nestjs/throttler seems, at the time of writing this article, to be having dependency conflicts with the latest Nest version.
Nest provides the module @nestjs/swagger, which allows generating the OpenAPI specification using decorators, and thus, automatically generating the documentation for our REST API.
To get started, add the following code to your main.ts file’s bootstrap function before app.listen().
const config = new DocumentBuilder()
.setTitle(APP_NAME)
.setDescription(APP_DESCRIPTION)
.setVersion(APP_VERSION)
.addBearerAuth() // The API will use Bearer Authentication
.addBasicAuth({ type: 'apiKey', name: 'accessToken', in: 'query' }) // The API will use basic authentication for admin access
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);
Our API’s documentation will, now, be accessible at the /docs path.
The APP_NAME, APP_DESCRIPTION and APP_VERSION in the code above, are imported from the src/common/constants.ts file that we set up earlier and the DocumentBuilder and SwaggerModule objects are imported from the @nestjs/swagger module.
From here we shall be using decorators on controllers, their methods, and data transfer objects (DTO) to enrich the API’s documentation.
You may read more about implementing the OpenAPI specification(OAS) to a NestJS app here.
An important way to protect our API is to restrict access to certain routes:
####Users & Authentication
To get started, generate our needed modules by running nest g resource users then nest g module auth. You’ll be prompted with two questions when generating the users' module, respond with “REST API” to the first and with “Y” to the second.
We won’t be able to go through every step in detail, check this GitHub repository for the code reference and follow the steps below:
src/common/dto and src/common/models to your project. These files are used in more than one place, they are in the common folder to avoid pointless code repetition.user.entity.ts file as it’s done here.user.model.ts file in the src/users/models directory and add the user model properties to it; Do the same for the users.model.ts file. These are the files that describe request responses, mostly for API documentation; Which is why there are many decorators describing each property.users.service.ts file then the users.controller.ts file by implementing their methods.account.controller.ts file in the users module to hold the methods for user account management: profile update, etc. Ignore the account balance method at this stage.account.controller.ts, the users.controller.ts and the users.service.ts to the users.module.ts file. Make sure this module exports TypeOrmModule and UsersService and imports TypeOrmModule.forFeature([User]) only at this stage. The exports ensure that whichever module that imports the current module can benefit from its exported providers.auth folder and add the login.dto.ts file and connection.model.ts file.auth.service.ts file, the auth.controller.ts and the auth.module.ts file as done here.jwt.strategy.ts then the user-auth.guard.ts file, the admin-auth.guard.ts file, and the current-user.decorator.ts file.At this stage, the API’s authentication is properly set up; we can now add other modules.
####Other modules
This is where you add all the other modules based on the data model of the API you’re building. For our simple accounting app, we’ll just add one module: The transactions module.
First, run nest g resource transactions, then go inside the generated transactions module and follow the steps below:
enums in the transactions folder and place both the transaction-type.enum.ts and the transaction-category.enum.ts files inside.transaction.model.ts and transactions.model.ts files in the src/transactions/models directory and add model fields to it.transactions.service.ts file then the transactions.controller.ts file by implementing their methods.transactions.controller.ts and the transactions.service.ts to the transactions.module.ts file. Make sure this module exports TypeOrmModule and TransactionsService and imports TypeOrmModule.forFeature([Transaction]) .TransactionsModule to the imports array of the UsersModule so that, from the AccountController we can access the TransactionsService from the TransactionsModule which contains the method we need for calculating the user account balance.getAccountBalance method in the account.controller.ts file as done here.
These steps would apply to almost any other module you’d add as long as you understand how all these files fit together.Note: Feel free to change the architecture of the project; just make sure you follow the basic rules of writing clean code.
There are many different ways to deploy APIs and many platforms to deploy them to. For this tutorial, I chose to show you how to deploy to Heroku because it has a free option and it’s very simple to get started with. I plan to write another article that will cover the deployment of REST APIs on the most popular platforms like AWS.
Let’s make our deployment setup and make sure it’s linked to GitHub:
First add the following to your package.json scripts: "preinstall": "rm -rf /dist" to the dist folder before installing dependencies and "postinstall": "npm run build && npm run migration:run"` to build the app and run migrations after dependencies are installed and before starting the app.
At the root of your project, create a file of the name:Procfile(no extension). Then paste the following inside:web: npm run start:prod. This will tell **Heroku** to start our app once it’s deployed.
Create a folder named.githubat the root folder of your project, create another folder namedworkflowsinside the.githubfolder, then add a file of the namemain.ymlinside theworkflowsfolder.
Paste the following in themain.yml``` file:
name: Deploy
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: akhileshns/heroku-deploy@v3.12.12
with:
heroku_api_key: ${{secrets.HEROKU_API_KEY}}
heroku_app_name: "cb-accounting-app-api"
heroku_email: ${{secrets.HEROKU_USER_EMAIL}}
This is a GitHub workflow, based on which, GitHub will deploy the app to Heroku every time there’s a change on the main branch of our API’s repository.
Do not forget to build and generate migrations before deploying the API. You need to build before because we are generating based on the JS versions of our API’s entities, available inside the /dist folder.
Before actually pushing the project to your GitHub repository with these settings, first make sure you have created a Heroku app to deploy to and have added its name and your Heroku API Key to your repository’s GitHub secrets.
Heroku Setup
Please feel free to skip this step if you know how to create a Heroku app.
You may find your Heroku Database credentials by going to Heroku Data, clicking on the database assigned to your application then selecting the “Settings” tab.
Adding the GitHub secrets
mail.yml file there.Your Heroku API key can be found in your Heroku user account settings.
Looks like we’re all set! You may now push your app to the GitHub repository and have it deployed to Heroku automatically.
Using NestJS can significantly help you structure your NodeJS API project better but it takes more than just using good libraries and frameworks to build a quality and secure API.
Thanks for following this tutorial, if you found it helpful please feel free to like or comment. All your suggestions or comments are welcome. You can also connect with me on Twitter, LinkedIn or GitHub.
Cheers!