DEV Community

shanmugharaj
shanmugharaj

Posted on

Building an api with NestJS (For beginners)

In this tutorial we will learn how to create a REST api that performs crud operations using TypeORM and postgresql. We will cover how to setup TypeORM, creating entities, repositories and use in our services to fetch data and integrate with our controllers.

Introduction

NestJS is a framework for building server sider applications using nodejs. It gives us the flexibility to choose ExpressJS or Fastify as the underlying library for it. It also comes with a cli which helps creating new project, module, service etc. Its also has full typescript support and gives us a opinionated way of structuring our applications that scale very well. we will see in action by creating a simple crud api from scratch.

Prerequesties

Basic knowledge about nodejs, rest api. npm, nodejs, postgresql installed in a machine.

Creating a new project

First we need to install nest-cli using npm i -g @nestjs/cli. After that create new project by running nest new <project name>. we will call our project as blog-api. Navigate to the desired location and run

nest new blog-api
Enter fullscreen mode Exit fullscreen mode

Now we get the following folder structure.

cli-project-structure

The main.ts file is our entry point and it has

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}

bootstrap();
Enter fullscreen mode Exit fullscreen mode

AppModule is imported and passed to the create method. Inside AppModule we see

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

controllers, services are added in the Module decorator. So basically all our app metadata is being passed to this AppModule and passed to the NestFactory.create method which gives an app instance for us.

Now run the project using npm run start:dev. Hit http://localhost:3000 in browser. Now we will see Hello World!. Press CTRL + C in terminal to quit the app from running.

We can delete some default files/folders that we are not interested in. Delete test, .git folders, app.controller.spec.ts, app.controller.ts, app.service.ts files. Now if we run our application it will no longer work. Since we deleted few files that's there in the AppModule as dependencies. To fix that update the app.module.ts with the following

import { Module } from '@nestjs/common';

@Module({
  imports: [],
  controllers: [],
  providers: [],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Now if we run we won't any errors in the terminal but we will see

{"statusCode":404,"message":"Cannot GET /","error":"Not Found"}
Enter fullscreen mode Exit fullscreen mode

in the browser if we hit http://localhost:3000. That's because we don't have any route attached to our app. We will add one shortly.

Creating posts resource

We can create all the necessary files and folders on our own or we can use cli to generate the scaffold for us. In this tutorial we will see how to create one using the cli. Resource means controllers, services, dtos, entities required for a module. To generate post resource run

nest g resource posts --no-spec
Enter fullscreen mode Exit fullscreen mode

It will ask the following questions

What transport layer do you use? - select REST API

Would you like to generate CRUD entry points? - type Y and then hit enter.

Now there are lot of files got generated inside the folder posts for us. We will go through each of them when we work on those files. If we navigate to update-post.dto.ts we see a package @nestjs/mapped-types is used and we didn't install that earlier. So install that by running

npm add @nestjs/mapped-types
Enter fullscreen mode Exit fullscreen mode

Now run the project and navigate to http://localhost:3000/posts and we will see This action returns all posts in the browser.

Setting typeorm with postgresql

Lets install the dependencies first by running

npm install --save typeorm pg @nestjs/typeorm @nestjs/config
Enter fullscreen mode Exit fullscreen mode

Create a .development.env file in the project root and add the following

DB=blog-api
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=<username here>
DB_PASSWORD=<password here>
DB_SYNCHRONIZE=true
DB_LOGGING=true
DB_AUTOLOADENTITIES=true
Enter fullscreen mode Exit fullscreen mode

In real world scenario we can choose not to add password here. We can set a environment variable with DB_PASSWORD in the terminal where we are running the application.

After that create a database named blog-api in the postgreql. Now lets create a typeorm config file in the location as shown below

typeorm-config-file

In the typeorm-config.service.ts add the following

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';

@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  constructor(private _configService: ConfigService) {}

  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      type: 'postgres',
      host: this._configService.get('DB_HOST'),
      port: +this._configService.get<number>('DB_PORT'),
      username: this._configService.get('DB_USERNAME'),
      password: this._configService.get('DB_PASSWORD'),
      database: this._configService.get('DB'),
      entities: [__dirname + '/../**/*.entity{.ts,.js}'],
      synchronize: !!this._configService.get('DB_SYNCHRONIZE'),
      logging: !!this._configService.get('DB_LOGGING'),
      autoLoadEntities: !!this._configService.get('DB_AUTOLOADENTITIES'),
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

@Injectable() tells NestJS that this class can be used in other areas using dependency injection. As we can see config service is used as

constructor(private _configService: ConfigService) {}
Enter fullscreen mode Exit fullscreen mode

this _configService instance will be created by NestJS for us. Since we made this TypeOrmConfigService as injectable we can use it similarly like config service in other areas of our app.

Now we need to add dependencies to use config service, typeorm in AppModule with the following

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';

import { TypeOrmConfigService } from './persistence/typeorm-config.service';
import { PostsModule } from './posts/posts.module';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true, envFilePath: '.development.env' }),
    TypeOrmModule.forRootAsync({
      useClass: TypeOrmConfigService,
    }),
    PostsModule,
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Creating posts entity

It's time to create our posts entity. Replace the posts.entity.ts with

import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryColumn,
  UpdateDateColumn,
} from 'typeorm';

@Entity('posts')
export class PostsEntity {
  @PrimaryColumn({ generated: true })
  id: number;

  @Column()
  content: string;

  @Column()
  title: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

@Entity() is to tell typeorm that this class should be mapped to db table and @Column() as db column. Now we want to id should be primary auto generated column so we decorated with @PrimaryColumn({ generated: true })

Now run the application using npm run dev:api and see in the terminal bunch of SQL queries printed by typeorm log. Now if we check our blog-api database in postgesql server, the table posts should have got created.

Update the CreatePostDto

update the create-post.dto.ts with the following

export class CreatePostDto {
  title: string;

  content: string;
}
Enter fullscreen mode Exit fullscreen mode

we don't need to change the UpdatePostDto for now since it already inherits the CreatePostDto with properties as optional.

Update the PostsService

In order to do any db operations we should have connection object from typeorm. There are multiple ways to fetch/update data on an entity. Typeorm also supports repository pattern. We will see more details of typeorm specifics in a separate tutorial. Now we will focus on a simple approach using repository pattern.

First we will create a private variable to store the repository instance of PostsEntity. Add the following in the posts.service.ts

//...
// ... existing imports ...
// ..
import { Repository, Connection } from 'typeorm';
import { PostsEntity } from './entities/post.entity';

export class PostsService {
  private _postsRepository: Repository<PostsEntity>;

  constructor(private _connection: Connection) {
    this._postsRepository = this._connection.getRepository(PostsEntity);
  }

  // ... 
  // ... existing code
  // ...
Enter fullscreen mode Exit fullscreen mode

The _connection is the postgres connection instance from typeorm. This will be injected by the NestJS for us. Now lets fill all the placeholder methods which was created by nest-cli.

async create(createPostDto: CreatePostDto) {
  // == creates a new entity instance ==
  const newPost = this._postsRepository.create();

  newPost.title = createPostDto.title;
  newPost.content = createPostDto.content;

  // == saves the post to db ==
  await this._postsRepository.save(newPost);
  return newPost;
}

async findAll() {
  // == returns all records if didn't specify any options ==
  return await this._postsRepository.find();
}

async findOne(id: number) {
  return await this._postsRepository.findOne(id);
}

async update(id: number, updatePostDto: UpdatePostDto) {
  // == if not throws an error ==
  const post = await this._postsRepository.findOneOrFail(id);

  post.content = updatePostDto.content;
  post.title = updatePostDto.title;

  await this._postsRepository.save(post);
  return post;
}

async remove(id: number) {
  await this._postsRepository.delete(id);
}
Enter fullscreen mode Exit fullscreen mode

Yayyy, that's it now we are done with code. We don't any update in the cotroller since its already wired up.

Testing the apis (Optional)

Now we can test this api with all routes are working or not. This portion is optional feel free to use any tool you like to test like postman etc. But I like a vscode extension REST client can be found here https://marketplace.visualstudio.com/items?itemName=humao.rest-client.

Create a new folder in the root api-queries and create a file inside it as queries.http and paste the follow in it.

@host=http://localhost:3000
###

# @name Posts
GET {{host}}/posts HTTP/1.1

###

# @name FetchPostByID
GET {{host}}/posts/1 HTTP/1.1

###

# @name NewPost
POST {{host}}/posts HTTP/1.1
content-type: application/json

{
  "title": "Rust Introduction",
  "content": "Hello, world rust"
}
###

# @name UpdatePost
PUT {{host}}/posts/1 HTTP/1.1
content-type: application/json

{
  "title": "ReactJS Hooks tutorial",
  "content": "First create a new project with cli"
}
###

# @name DeletePost
DELETE {{host}}/posts/1 HTTP/1.1
content-type: application/json

###
Enter fullscreen mode Exit fullscreen mode

If we open this file in vscode we will see like

queries.http

click the Send Request and you will see the results in the right pane. we can play around with all the apis with this.

We will see how to add authentication with Passport + JWT in next tutorial.

Happy Coding!

Top comments (0)