Download the installation package for your OS.
Docker Desktop includes Docker Compose, so we do not need to install Compose separately. If you installed Docker in another way and don't have Compose yet, please consult the Install Docker Compose documentation.
A basic understanding of the Docker CLI and Docker Compose is also a prerequisite.
Add a docker-comppose.yml
file to the root of the repository and add the following configuration to it.
version: '3.5'
services:
postgres:
container_name: postgres
image: postgres:14.5-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: acme
PGDATA: /data/postgres
volumes:
- postgres:/data/postgres
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- 5432:5432
restart: always
volumes:
postgres:
Take note of the following three environment variables in the Compose configuration file:
POSTGRES_USER
: superuser username for PostgreSQLPOSTGRES_PASSWORD
: superuser password for PostgreSQLPOSTGRES_DB
: name of the Postgres database (hardcoded toacme
)
Be sure to configure these environment variables locally before starting the container. (Tip: I use direnv to load and unload environment variables depending on the current directory.)
The database name is set to acme
and there is also a volume which points to an init.sql
SQL script. Add a new file called init.sql
to the root of the repository and add the following DDL to it.
CREATE TABLE "car_insurance_quote" (
"id" SERIAL NOT NULL,
"ageofdriver" INTEGER NOT NULL,
"monthlypremium" DECIMAL(12,2) NOT NULL,
"yearlypremium" DECIMAL(12,2) NOT NULL,
"createdon" TIMESTAMP(6) NOT NULL,
PRIMARY KEY ("id")
);
When you start the Docker Container the first time this init.sql
script will be executed and a table called car_insurance_quote
will be created. We will be saving the car insurance quotes in this table later.
Run the following command to run PostgreSQL in a Docker Container:
docker-compose up -d
Verify that PostgreSQL is up and running.
docker ps
docker-compose logs -f --tail 50 postgres
Using a tool such as DBeaver you can connect to the local PostgreSQL instance and verify that the acme
database was created and that it contains the car_insurance_quote
table.
NestJS is database agnostic, allowing you to easily integrate with any SQL or NoSQL database. We can use any Node.js
database library or ORM such as Sequelize, Prisma, Knex.js...etc. For this course, we'll be using Knex.js
, which is a SQL query builder with support for PostgreSQL
, MSSQL
, MySQL
, MariaDB
, SQLite3
, Oracle
and Amazon Redshift
. As you might have guessed from the title of the chapter we'll be using PostgreSQL as a database.
To work with Knex.js
and PostgreSQL
we need to install a few packages.
yarn add pg knex nestjs-knex
Remark: The nestjs-knex package is not an official NestJS package, but it suffices to integrate Knex with our NestJS application for this course. In a real-world application, you might want to write your own NestJS package to integrate Knex. This package is not frequently updated, making it troublesome to update Knex.js
.
After installing the nestjs-knex
package we need to import it into our application's root module. Open the app.module.file
and modify it as listed below.
import { Module } from '@nestjs/common';
...
import { KnexModule } from 'nestjs-knex';
@Module({
imports: [
...
KnexModule.forRootAsync({
useFactory: () => ({
config: {
client: 'pg',
connection: {
host: process.env.POSTGRES_HOST,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
database: 'acme',
},
},
}),
}),
],
providers: [...]
})
export class AppModule {}
We reuse the POSTGRES_USER
and POSTGRES_PASSWORD
environment variables we declared earlier and add a new one named POSTGRES_HOST
. Add this new environment variable and point it to the host where the PostgreSQL database is running. In our current setup, this is localhost
.
Remark: The KnexModule
imports the KnexCoreModule
module which is a global module. Providers exported by global modules are registered in the global scope. Once loaded, they're available everywhere. We can inject these providers in any of our application's modules.
Now that the PostgreSQL database is up and running and Knex.js
has been introduced into the codebase we can finally persist the car insurance quotes. Add a new file called car-insurance-quote.repository.ts
to the repositories/
folder of the car insurance quote module and add the following code to it.
export interface CarInsuranceQuote {
id: number;
ageOfDriver: number;
monthlyPremium: number;
yearlyPremium: number;
createdOn: Date;
}
export abstract class CarInsuranceQuoteRepository {
public abstract save(
ageOfDriver: number,
monthlyPremium: number,
yearlyPremium: number
): Promise<CarInsuranceQuote>;
public abstract load(id: number): Promise<CarInsuranceQuote>;
}
We declare a simple repository contract using an abstract class. A cool feature of TypeScript is that you can also implement abstract classes, you are not limited to only implementing interfaces. TypeScript can extract the interface from the abstract class. The repository allows us to save and load car insurance quotes.
Let's implement a Knex specific implementation of this abstract class. Add a file called knex-car-insurance-quote.repository.ts
to the folder containing the abstract class. It contains the following code:
import { Injectable } from '@nestjs/common';
import { InjectKnex, Knex } from 'nestjs-knex';
import {
CarInsuranceQuote,
CarInsuranceQuoteRepository,
} from './car-insurance-quote.repository';
@Injectable()
export class KnexCarInsuranceQuoteRepository
implements CarInsuranceQuoteRepository
{
private table = 'car_insurance_quote';
constructor(@InjectKnex() private readonly knex: Knex) {}
public async save(
ageOfDriver: number,
monthlyPremium: number,
yearlyPremium: number
): Promise<CarInsuranceQuote> {
const createdOn: Date = new Date();
const result = await this.knex.table(this.table).insert(
{
ageofdriver: ageOfDriver,
monthlypremium: monthlyPremium,
yearlypremium: yearlyPremium,
createdon: createdOn,
},
'id'
);
return {
id: result[0].id,
ageOfDriver,
monthlyPremium,
yearlyPremium,
createdOn,
};
}
public async load(id: number): Promise<CarInsuranceQuote> {
const row = await this.knex
.table(this.table)
.where('id', id)
.select<CarInsuranceQuote>(
'id',
'ageofdriver as ageOfDriver',
'monthlypremium as monthlyPremium',
'yearlypremium as yearlyPremium',
'createdon as createdOn'
)
.first<CarInsuranceQuote>();
return row
? {
id: row.id,
ageOfDriver: row.ageOfDriver,
// TODO: use node-pg-types to configure parsers to convert PostgreSQL types back into JavaScript types.
monthlyPremium: parseFloat(row.monthlyPremium as any),
yearlyPremium: parseFloat(row.yearlyPremium as any),
createdOn: row.createdOn,
}
: undefined;
}
}
Via the @InjectKnex()
decorator provided by the nestjs-knex
package we inject a Knex
instance that allows us to work with the database via Knex.js
. The Knex
provider is part of the global Knex module we imported earlier in our application's root module. The implementation for the save()
and load()
methods use this instance to persist and retrieve the car insurance quotes.
Now that we provided an implementation for the abstract CarInsuranceQuoteRepository
class we need to register it in NestJS's dependency injection system. We are going to register it in the car insurance quote module, so open the car-insurance-quote.module.ts
file. We need to add a provider that instructs NestJS to supply an instance of the KnexCarInsuranceQuoteRepository
class whenever a CarInsuranceQuoteRepository
is injected.
...
import { CarInsuranceQuoteRepository } from './repositories/car-insurance-quote.repository';
import { KnexCarInsuranceQuoteRepository } from './repositories/knex-car-insurance-quote.repository';
@Module({
controllers: [QuoteController],
providers: [
CarBrandRepository,
QuoteService,
{
provide: CarInsuranceQuoteRepository,
useClass: KnexCarInsuranceQuoteRepository,
},
],
})
export class CarInsuranceQuoteModule {}
We are almost there. The only thing left to do is to update the quote service (quote.service.ts
) to use the new repository to save and load the quotes.
First, inject the repository via the service's constructor.
import { CarBrand, CarBrandRepository, CarInsuranceQuoteRepository } from './repositories';
@Injectable()
export class QuoteService {
constructor(
private readonly carBrandRepository: CarBrandRepository,
private readonly quoteRepository: CarInsuranceQuoteRepository,
) {}
...
}
Then modify the getById()
method to load quotes via the repository.
@Injectable()
export class QuoteService {
...
public async getById(id: number): Promise<Premium | undefined> {
const quote = await this.quoteRepository.load(id);
return quote
? {
id: quote.id,
monthlyPremium: quote.monthlyPremium,
yearlyPremium: quote.yearlyPremium,
}
: undefined;
}
}
And finally, update the calculatePremium()
method to persist the quotes. You can remove the private premiums
variable as we no longer store the premiums in-memory.
@Injectable()
export class QuoteService {
...
public async calculatePremium(
ageOfDriver: number,
carId: number,
purchasePrice: number,
): Promise<Premium> {
...
const premium = await this.quoteRepository.save(
ageOfDriver,
Math.round(brand.yearlyPremium / 12),
brand.yearlyPremium,
);
return {
id: premium.id,
monthlyPremium: premium.monthlyPremium,
yearlyPremium: premium.yearlyPremium,
};
}
}
Voila, the quotes are now persisted in the database. Start the application, open a browser and navigate to http://localhost:3000/api and use the Swagger UI to test it. Yay, the car insurance quotes are now persisted in a database!