5

API with NestJS #2. Setting up a PostgreSQL database with TypeORM

 3 years ago
source link: https://wanago.io/2020/05/18/api-nestjs-postgresql-typeorm/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

The next important thing when learning how to create an API is how to store the data. In this article, we look into how to do so with PostgreSQL and NestJS . To make the managing of a database more convenient, we use an Object-relational mapping (ORM) tool called TypeORM . To have an even better understanding, we also look into some SQL queries. By doing so, we can grasp what advantages ORM gives us.

You can find all of the below code in this repository .

Creating a PostgreSQL database

The most straightforward way of kickstarting our development with a Postgres database is to use docker .

Here, we use the same setup as in the TypesScript Express series .

The first thing to do is to install Docker and Docker Compose . Now, we need to create a docker-compose file and run it.

docker-compose.yml

version: "3"
services:
  postgres:
    container_name: postgres
    image: postgres:latest
    ports:
    - "5432:5432"
    volumes:
    - /data/postgres:/data/postgres
    env_file:
    - docker.env
    networks:
    - postgres
 
  pgadmin:
    links:
    - postgres:postgres
    container_name: pgadmin
    image: dpage/pgadmin4
    ports:
    - "8080:80"
    volumes:
    - /data/pgadmin:/root/.pgadmin
    env_file:
    - docker.env
    networks:
    - postgres
 
networks:
  postgres:
    driver: bridge

The useful thing about the above configuration is that it also starts a pgAdmin console. It gives us the possibility to view the state of our database and interact with it.

To provide credentials used by our Docker containers, we need to create the docker.env file. You might want to skip committing it by adding it to your  .gitignore .

docker.env

POSTGRES_USER=admin
POSTGRES_PASSWORD=admin
POSTGRES_DB=nestjs
[email protected]
PGADMIN_DEFAULT_PASSWORD=admin

Once all of the above is set up, we need to start the containers:

docker-compose up

Environment variables

A crucial thing to running our application is to set up environment variables . By using them to hold configuration data, we can make it easily configurable. Also, it is easier to keep sensitive data from being committed to a repository.

In the Express Typescript series , we use a library called dotenv to inject our variables. In NestJS, we have a  ConfigModule that we can use in our application. It uses dotenv under the hood.

npm install @nestjs/config

app.module.ts

import { Module } from '@nestjs/common';
import { PostsModule } from './posts/posts.module';
import { ConfigModule } from '@nestjs/config';
 
@Module({
  imports: [PostsModule, ConfigModule.forRoot()],
  controllers: [],
  providers: [],
})
export class AppModule {}

As soon as we create a .env file at the root of our application, NestJS injects them into a ConfigSerivice that we will use soon.

.env

POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=admin
POSTGRES_PASSWORD=admin
POSTGRES_DB=nestjs
PORT=5000

Validating environment variables

It is an excellent idea to verify our environment variables before running the application. In the TypeScript Express series , we’ve used a library called envalid .

The ConfigModule built into NestJS supports  @hapi/joi that we can use to define a  validation schema .

npm install @hapi/joi @types/hapi__joi

app.module.ts

import { Module } from '@nestjs/common';
import { PostsModule } from './posts/posts.module';
import { ConfigModule } from '@nestjs/config';
import * as Joi from '@hapi/joi';
 
@Module({
  imports: [
    PostsModule,
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        POSTGRES_HOST: Joi.string().required(),
        POSTGRES_PORT: Joi.number().required(),
        POSTGRES_USER: Joi.string().required(),
        POSTGRES_PASSWORD: Joi.string().required(),
        POSTGRES_DB: Joi.string().required(),
        PORT: Joi.number(),
      })
    })
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

Connecting a NestJS application with PostgreSQL

A first thing to do once we have our database running is to define a connection between our application and the database. To do so, we use TypeOrmModule .

npm install @nestjs/typeorm typeorm pg

To keep our code clean, I suggest creating a database module .

database.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
 
@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get('POSTGRES_HOST'),
        port: configService.get('POSTGRES_PORT'),
        username: configService.get('POSTGRES_USER'),
        password: configService.get('POSTGRES_PASSWORD'),
        database: configService.get('POSTGRES_DB'),
        entities: [
          __dirname + '/../**/*.entity.ts',
        ],
        synchronize: true,
      })
    }),
  ],
})
export class DatabaseModule {}

The synchronize flag above is very important. We will elaborate on it a lot later

An essential thing above is that we use the ConfigModule and ConfigService . The  useFactory method can access the environment variables thanks to providing the  imports and  inject arrays. We elaborate on these mechanisms in the upcoming parts of this series.

Now, we need to import our DatabaseModule .

app.module.ts

import { Module } from '@nestjs/common';
import { PostsModule } from './posts/posts.module';
import { ConfigModule } from '@nestjs/config';
import * as Joi from '@hapi/joi';
import { DatabaseModule } from './database/database.module';
 
@Module({
  imports: [
    PostsModule,
    ConfigModule.forRoot({
      validationSchema: Joi.object({
        POSTGRES_HOST: Joi.string().required(),
        POSTGRES_PORT: Joi.number().required(),
        POSTGRES_USER: Joi.string().required(),
        POSTGRES_PASSWORD: Joi.string().required(),
        POSTGRES_DB: Joi.string().required(),
        PORT: Joi.number(),
      })
    }),
    DatabaseModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

Entities

The most crucial concept to grasp when using TypeORM is the entity . It is a class that maps to a database table. To create it, we use the  @ Entity ( ) decorator.

post.entity.ts

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
 
@Entity()
class Post {
  @PrimaryGeneratedColumn()
  public id: number;
 
  @Column()
  public title: string;
 
  @Column()
  public content: string;
}
 
export default Post;

A neat thing about TypeORM is that it integrates well with TypeScript because it is written in it. To define our columns, we can use various decorators.

@PrimaryGeneratedColumn()

A primary key is a column used to identify a row uniquely in a table. Although we might use an existing column and make it primary, we usually create an  id column. By choosing  PrimaryGeneratedColumn from TypeORM, we create an integer primary column that has a value generated automatically.

@Column()

The @ Column ( ) decorator marks a property as a column. When using it, we have two possible approaches.

The first approach is not to pass the column type explicitly. When we do it, TypeORM figures out the column using our TypeScript types. It is possible because NestJS uses reflect-metadata under the hood.

The second approach would be to pass the type of column explicitly, for example, by using @ Column ( 'text' ) . The available column types differ between databases like MySQL and Postgres. You can look them up in the TypeORM documentation .

It is a proper moment to discuss different ways to store strings in Postgres. Relying on TypeORM to figure out the type of a string column results in the “character varying” type, also called varchar .

VnqUJvN.png!web

Varchar is very similar to a text type of a column but gives us a possibility to limit the length of a string. Both types are the same performance-wise.

SQL query

In pgAdmin, we can check a query equivalent to what TypeORM did for us under the hood.

CREATE TABLE public.post
(
    id integer NOT NULL DEFAULT nextval('post_id_seq'::regclass),
    title character varying COLLATE pg_catalog."default" NOT NULL,
    content character varying COLLATE pg_catalog."default" NOT NULL,
    CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY (id)
)

There are a few interesting things to notice above

Using @ PrimaryGeneratedColumn ( ) results in having an  int column. It defaults to the return value of a  nextval function that returns unique ids. An alternative would be to use a  serial type instead and would make the query shorter, but it works the same under the hood.

Our entity has varchar columns that use COLLATE . Collation is used to specify the sort order and character classification. To see our default collation, we can run this query:

SHOW LC_COLLATE
en_US.utf8

The above value is defined in a query that was used to create our database. It is UTF8 and English by default.

CREATE DATABASE nestjs
    WITH 
    OWNER = admin
    ENCODING = 'UTF8'
    LC_COLLATE = 'en_US.utf8'
    LC_CTYPE = 'en_US.utf8'
    TABLESPACE = pg_default
    CONNECTION LIMIT = -1;

Also, our CREATE TABLE query puts a  constraint on our ids so that they are always unique.

PK_be5fda3aac270b134ff9c21cdee is a name of the above constraint and was generated

Repositories

With repositories, we can manage a particular entity. A repository has multiple functions to interact with entities. To access it, we use the  TypeOrmModule again.

posts.module.ts

import { Module } from '@nestjs/common';
import PostsController from './posts.controller';
import PostsService from './posts.service';
import Post from './post.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
 
@Module({
  imports: [TypeOrmModule.forFeature([Post])],
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}

Now, in our PostsService , we can inject the Posts repository.

import { InjectRepository } from '@nestjs/typeorm';
constructor(
  @InjectRepository(Post)
  private postsRepository: Repository<PostEntity>
) {}

Finding

With the find function, we can get multiple elements. If we don’t provide ith with any options, it returns all.

getAllPosts() {
  return this.postsRepository.find();
}

To get just one element we use the findOne function. By providing it with a number we indicate that we want an element with a particular id. If the result is undefined, it means that the element wasn’t found.

async getPostById(id: number) {
  const post = await this.postsRepository.findOne(id);
  if (post) {
    return post;
  }
  throw new HttpException('Post not found', HttpStatus.NOT_FOUND);
}

Creating

By using the create function, we can instantiate a new Post. We can use the save function afterward to populate the database with our new entity.

async createPost(post: CreatePostDto) {
  const newPost = await this.postsRepository.create(post);
  await this.postsRepository.save(newPost);
  return newPost;
}

Modifying

To modify an existing element, we can use the update function. Afterward, we would use the  findOne function to return the modified element.

async updatePost(id: number, post: UpdatePostDto) {
  await this.postsRepository.update(id, post);
  const updatedPost = await this.postsRepository.findOne(id);
  if (updatedPost) {
    return updatedPost
  }
  throw new HttpException('Post not found', HttpStatus.NOT_FOUND);
}

A significant thing is that it accepts a partial entity, so it acts as a PATCH, not as a PUT. If you want to read more on PUT vs PATCH (although with MongoDB), check out TypeScript Express tutorial #15. Using PUT vs PATCH in MongoDB with Mongoose

Deleting

To delete an element with a given id, we can use the delete function.

async deletePost(id: number) {
  const deleteResponse = await this.postsRepository.delete(id);
  if (!deleteResponse.affected) {
    throw new HttpException('Post not found', HttpStatus.NOT_FOUND);
  }
}

By checking out the documentation of the DELETE command , we can see that we have access to the count of removed elements. This data is available in the  affected property. If it equals zero, we can assume that the element does not exist.

Handling asynchronous errors

A beneficial thing about NestJS controllers is that they handle asynchronous errors very well.

@Get(':id')
getPostById(@Param('id') id: string) {
  return this.postsService.getPostById(Number(id));
}

If the getPostById function throws an error, NestJS catches it automatically and parses it. When using pure Express, we would do this ourselves:

getAllPosts = async (request: Request, response: Response, next: Next) => {
  const id = request.params.id;
  try {
    const post = await this.postsService.getPostById();
    response.send(post);
  } catch (error) {
    next(error);
  }
}

Summary

In this article, we’ve gone through the basics of connecting our NestJS application with a PostgreSQL database. Not only did we use TypeORM, but we’ve also looked into some SQL queries. NestJS and TypeORM have lots of features built-in and ready to use. In the upcoming parts of this series, we will look into them more, so stay tuned!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK