In this lesson, the application will be connected to a database, utilizing TypeORM as the bridge between object-oriented code and SQL queries in a Nest.js context. As a best practice, isolating database connection configurations into environment variables ensures both security and easier environment management. Learning how to connect Nest.js applications to a Postgres database provides a comprehensive understanding of backend architecture, an area where Nest.js offers more out of the-box features compared to Express.
Install Dependencies
"@nestjs/typeorm": "^9.0.1",
"pg": "^8.10.0",
"typeorm": "^0.3.15"
Should TypeScript typings for TypeORM be absent, installation as a devDependency is possible.
"devDependencies": {
"@types/node": "^18.11.18"
}
First, installation of the required dependencies in the application is necessary. These packages can be copied into the package.json file, followed by running the command npm install.
Import TypeORM Module to App Module
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.
Test the DB Connection
export class AppModule implements NestModule {
constructor(private dataSource: DataSource) {
console.log(dataSource.driver.database);
}
}
Testing the database connection can be accomplished by injecting the datasource class into AppModule, followed by logging the name of the database. Upon running the application, the database name will appear in the logs, signifying a successful connection between the NestJS application and the 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 is a class that maps to a database table, or to a collection when using MongoDB. In the context of NestJS, which often pairs with Object-Relational Mapping tools like TypeORM, an entity functions as the data model for the application, interfacing directly with the database schema. This exemplifies the software engineering principle of data source abstraction, allowing for easier swapping of databases or migration. As a best practice, encapsulating database interactions within repository classes streamlines code maintenance and enhances testability.
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"
@Entity("songs")
export class Song {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column("varchar", { array: true })
artists: string[];
@Column({ type: "date" })
releasedDate: Date;
@Column({ type: "time" })
duration: Date;
@Column({ type: "text" })
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.
@Column('varchar', { array: true })
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.
@Column({ type: 'date' })
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
@Column({ type: 'time' })
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.
@Column({ type: 'text' })
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()
readonly lyrics: string;
A new field, lyrics, has been added to store the lyrics in the database. This field was not previously included in the CreateSongDTO file.
Register the Entity in AppModule
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],
Test the Application
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.
Create and Fetch Records from DB
Repository Pattern
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:
import { Song } from "./song.entity";
import { InjectRepository } from "@nestjs/typeorm";
export class SongsService {
constructor(
@InjectRepository(Song)
private songRepository: Repository<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
async create(songDTO: CreateSongDto): Promise<Song> {
const song = new Song();
song.title = songDTO.title;
song.artists = songDTO.artists;
song.duration = songDTO.duration;
song.lyrics = songDTO.lyrics;
song.releasedDate = songDTO.releasedDate;
return await this.songRepository.save(song);
// 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.
A database promise is essential for handling asynchronous operations when interacting with a database. In a web application, tasks like querying, updating, or deleting records are not instantaneous and can take an undefined amount of time to complete. Utilizing promises allows your application to continue executing other tasks while waiting for the database operation to resolve, thereby improving performance and user experience.
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
findOne(id: number): Promise<Song> {
return this.songRepository.findOneBy({ id });
}
You can find the record by id by using findOneBy() from the songsRepository.
// songs.controller.ts
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number): Promise<Song> {
return this.songsService.findOne(id);
}
Delete Record
// songs.service.ts
async remove(id: number): Promise<void> {
await this.songRepository.delete(id);
}
songRepository provides the delete method to delete the record based on id.
// songs.controller.ts
@Delete(':id')
delete(@Param('id', ParseIntPipe) id: number): Promise<void> {
return this.songsService.remove(id);
}
Update Record
//songs.service.ts
import { Repository, UpdateResult } from 'typeorm';
import { UpdateSongDto } from './dto/update-song.dto';
update(id: number, recordToUpdate: UpdateSongDto): Promise<UpdateResult> {
return this.songRepository.update(id, recordToUpdate);
}
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.
Create another DTO object to update the record.
// src/songs/dto/update-song.dto.ts
import {
IsArray,
IsDateString,
IsMilitaryTime,
IsNotEmpty,
IsOptional,
IsString,
} from "class-validator";
export class UpdateSongDto {
@IsString()
@IsOptional()
readonly title;
@IsOptional()
@IsArray()
@IsString({
each: true
})
readonly artists;
@IsDateString()
@IsOptional()
readonly releasedDate: Date;
@IsMilitaryTime()
@IsOptional()
readonly duration: Date;
@IsString()
@IsOptional()
readonly lyrics: string;
}
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(
@Param('id', ParseIntPipe) id: number,
@Body() updateSongDTO: UpdateSongDto,
): Promise < UpdateResult > {
return this.songsService.update(id, updateSongDTO);
}
Test the Application
PUT http://localhost:3001/songs/3
Content-Type: application/json
{
"title": "You for Me 3",
"artists": ["Siagla", "Yan", "Ny"],
"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
Install Pagination Package
You are going to use nestjs-typeorm-paginate to implement pagination. The nestjs-typeorm-paginate package offers several features that make it advantageous over implementing pagination manually:
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
Create a new paginate method in songs.service.ts
import {
paginate,
Pagination,
IPaginationOptions,
} from 'nestjs-typeorm-paginate';
async paginate(options: IPaginationOptions): Promise<Pagination<Song>> {
// Adding query builder
// If you need to add query builder you can add it here
return paginate<Song>(this.songRepository, options);
}
Refactor findAll method in controller
// songs.controller.ts
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe)
page: number = 1,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe)
limit: number = 10,
): Promise < Pagination < Song >> {
limit = limit > 100 ? 100 : limit;
return this.songsService.paginate({
page,
limit,
});
}
Refactor the findAll method in songs.controller.ts to obtain page and limit from the request query parameters. Use the @Query decorator to capture these query parameters.
Set default values for page and limit, and then call the paginate method from songService, passing in the page and limit values.
Test the Application
Now test the application by sending request http://localhost:3001/songs/?page=2&limit=2
Specify the page and limit value on the based on your situation/usecase
This is the response:
{
"items": [
{
"id": 10,
"title": "A",
"artists": ["New Artists"],
"releasedDate": "2022-09-29",
"duration": "05:00:00",
"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"
},
{
"id": 5,
"title": "Lasting Lover",
"artists": ["Siagla"],
"releasedDate": "2022-09-29",
"duration": "02:34:00",
"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"
}
],
"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>> {
const queryBuilder = this.songRepository.createQueryBuilder('c');
queryBuilder.orderBy('c.releasedDate', 'DESC');
return paginate<Song>(queryBuilder, options);
}
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.
$7 bundle of my best NestJS + backend developer Courses.
More details coming soon.
Get the latest insights from the marketing world.