Chapter 5. TypeORM

Almost every time you use Nest.js in the real world, you need some kind of persistence for your data. That is, you need to save the data that the Nest.js app receives somewhere, and you need to read data from somewhere so that you can then pass that data as a response to the requests that the Nest.js app receives.

That “somewhere” will be, most of the time, a database.

TypeORM is a Object Relational Mapping (ORM) that works with several different relational databases. An Object Relational Mapping is a tool that converts between objects (such as “Entry” or “Comment,” since we’re building a blog) and tables in a database.

The result of this conversion is an entity (called Data Transfer Object) that knows how to read data from the database to memory (so you can use the data as a response for a request,) as well as how to write to the database from memory (so that you are able to store data for later).

TypeORM is conceptually similar to Sequelize. TypeORM is also written in TypeScript and uses decorators extensively, so it’s a great match for Nest.js projects.

We will obviously focus on using TypeORM together with Nest.js, but TypeORM can also be used in both the browser and the server side, with traditional JavaScript as well as TypeScript.

TypeORM allows you to use both the data mapper pattern, as well as the active record pattern. We will focus on the active record pattern as it greatly reduces the amount of boilerplate code needed to use in the context of a typical Nest.js architecture, like the one explained throughout the book.

TypeORM can also work with MongoDB, though in this case using a dedicated NoSQL ORM such as Mongoose is a more common approach.

What database to use

TypeORM supports the following databases:

  • MySQL
  • MariaDB
  • PostgreSQL
  • MS SQL Server
  • sql.js
  • MongoDB
  • Oracle (experimental)

Considering that in this book we are already using PostgreSQL with Sequelize and MongoDB with Mongoose, we decided to use MariaDB with TypeORM.

About MariaDB

MariaDB is an open source, community-driven project led by some of the original developers of MySQL. It was forked from MySQL when Oracle acquired the latter with the intention of keeping it free and open under the GNU General Public License.

The original idea of the project was to act as a drop-in replacement for MySQL. This remains largely true for version up to 5.5, while MariaDB kept its version numbers in sync with the MySQL ones.

Nevertheless, newer versions, starting with versions 10.0, have slightly diverted from this approach. It’s still true, though, that MariaDB still focuses on being highly compatible with MySQL and sharing the same API.

Getting started

TypeORM is of course distributed as an npm package. You need to run npm install typeorm @nestjs/typeorm.

You also need a TypeORM database driver; in this case, we will install the MySQL/MariaDB one with npm install mysql.

TypeORM depends on reflect-metadata as well, but luckily we had it previously installed as Nest.js depends on it too, so there’s nothing else for us to do. Keep in mind that you will need to install this dependency too if you’re using TypeORM outside of a Nest.js context.

NOTE: If you haven’t yet, it’s always a good idea to install Node.js: npm install --save-dev @types/node.

Start the database

In order to get a database to connect to, we will use Docker Compose, with the official MariaDB Docker image, to set up our local development environment. We will point to the latest Docker image tag, which at the time of writing, corresponds to version 10.2.14.

version: '3'

volumes:
  # for persistence between restarts
  mariadb_data:

services:
  mariadb:
    image: mariadb:latest
    restart: always
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: nestbook
      MYSQL_USER: nest
      MYSQL_PASSWORD: nest
    volumes:
        - mariadb_data:/var/lib/mysql

  api:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - NODE_ENV=development
    depends_on:
      - mariadb
    links:
      - mariadb
    environment:
      PORT: 3000
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    command: >
      npm run start:dev

Connect to the database

Now that we have a database to connect TypeORM, let’s configure the connection.

We have several ways of configuring TypeORM. The most straightforward one, which is great for getting started, is creating a ormconfig.json file in the project root folder. This file will get grabbed automagically by TypeORM on startup.

Here is an example configuration file that suits our usecase (i.e. using Docker Compose with the configuration previously proposed).

ormconfig.json

{
  "type": "mariadb",
  "host": "mariadb",
  "port": 3306,
  "username": "nest",
  "password": "nest",
  "database": "nestbook",
  "synchronize": true,
  "entities": ["src/**/*.entity.ts"]
}

Some notes on the configuration file:

  • The properties host, port, username, password and database need to match the ones specified earlier in the docker-compose.yml file; otherwise, TypeORM will not be able to connect to the MariaDB Docker image.
  • The synchronize property tells TypeORM whether to create or update the database schema whenever the application starts, so that the schemas match the entities declared in the code. Setting this property to true can easily lead to loss of data, so make sure you know what you’re doing before enabling this property in production environments.

Initialize TypeORM

Now that the database is running and you are able to successfully establish a connection between it and our Nest.js app, we need to instruct Nest.js to use TypeORM as a module.

Thanks to the @nest/typeorm package we previously installed, using TypeORM inside our Nest.js application is as easy as importing the TypeOrmModule in our main app module (probably the app.module.ts file.)

import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot(),
    ...
  ]
})

export class AppModule {}

Modelling our data

Probably the best thing about using an ORM is that you can take advantage of the modelling abstraction that they provide: basically, they allow us to think about our data and to shape it with properties (including types and relations), generating “object types” (and plugging them to databases tables) that we can then use and manipulate as direct interfaces.

This abstraction layer saves you from writing database-specific code like queries, joins, etc. A lot of people love not having to struggle with selects and the like; so this abstraction layer comes in handy.

Our first entity

When working with TypeORM, this object abstractions are named entities.

An entity is basically a class that is mapped to a database table.

With that said, let’s create our first entity, which we will name Entry. We will use this entity to store entries (posts) for our blog. We will create a new file at src/entries/entry.entity.ts; that way TypeORM will be able to find this entity file since earlier in our configuration we specified that entity files will follow the src/**/*.entity.ts file naming convention.

import { Entity } from 'typeorm';

@Entity()
export class Entry {}

The @Entity() decorator from the typeorm npm package is used to mark the Entry class as an entity. This way, TypeORM will know that it needs to create a table in our database for these kinds of objects.

The Entry entity is still a bit too simple: we haven’t defined a single property for it. We will probably need things like a title, a body, an image and a date for our blog entries, right? Let’s do it!

import { Entity, Column } from 'typeorm';

@Entity()
export class Entry {
  @Column() title: string;

  @Column() body: string;

  @Column() image: string;

  @Column() created_at: Date;
}

Not bad! Each property we define for our entity is marked with a @Column decorator. Again, this decorator tells TypeORM how to treat the property: in this case, we are asking for each property to be stored in a column of the database.

Sadly, this entity will not work with this code. This is because each entity needs to have at least one primary column, and we didn’t mark any column as such.

Our best bet is to create an id property for each entry and store that on a primary column.

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

@Entity()
export class Entry {
  @PrimaryColumn() id: number;

  @Column() title: string;

  @Column() body: string;

  @Column() image: string;

  @Column() created_at: Date;
}

Ah, that’s better! Our first entity is working now. Let’s use it!

Using our models

When having to connect requests to data models, the typical approach in Nest.js is building dedicated services, which serve as the “touch point” with each model, and to build controllers, which link the services to the requests reaching the API. Let’s follow the model -> service -> controller approach in the following steps.

The service

In a typical Nest.js architecture, the application heavy-lifting is done by the services. In order to follow this pattern, create a new EntriesService, using it to interact with the Entry entity.

So, let’s create a new file at: src/entries/entries.service.ts

import { Component } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { Entry } from './entry.entity';

@Component()
export class EntriesService {
  constructor(
    // we create a repository for the Entry entity
    // and then we inject it as a dependency in the service
    @InjectRepository(Entry) private readonly entry: Repository<Entry>
  ) {}

  // this method retrieves all entries
  findAll() {
    return this.entry.find();
  }

  // this method retrieves only one entry, by entry ID
  findOneById(id: number) {
    return this.entry.findOneById(id);
  }

  // this method saves an entry in the database
  create(newEntry: Entry) {
    this.entry.save(newEntry);
  }
}

The most important part of the service is creating a TypeORM repository with Repository<Entry>, and then injecting it in our constructor with @InjectRepository(Entry).

By the way, in case you’re wondering, repositories are probably the most commonly used design pattern when dealing with ORMs, because they allow you to abstract the database operations as object collections.

Coming back to the latest service code, once you have created and injected the Entry repository, use it to .find() and .save() entries from the database, among other things. These helpful methods are added when we create a repository for the entity.

Now that we have taken care of both the data model and the service, let’s write the code for the last link: the controller.

The controller

Let’s create a controller for exposing the Entry model to the outside world through a RESTful API. The code is really simple, as you can see.

Go ahead and create a new file at: src/entries/entries.controller.ts

import { Controller, Get, Post, Body, Param } from '@nestjs/common';

import { EntriesService } from './entry.service';

@Controller('entries')
export class EntriesController {
  constructor(private readonly entriesSrv: EntriesService) {}

  @Get()
  findAll() {
    return this.entriesSrv.findAll();
  }

  @Get(':entryId')
  findOneById(@Param('entryId') entryId) {
    return this.entriesSrv.findOneById(entryId);
  }

  @Post()
  create(@Body() entry) {
    return this.entriesSrv.create(entry);
  }
}

As usual, we are using Nest.js dependency injection to make the EntryService available in our EntryController.

Building a new module

The last step for our new entity endpoint to work is to include the entity, the service, and the controller in the app module. Instead of doing this directly, we will follow the “separated modules” approach and create a new module for our entries, importing all of the necessary pieces there and then importing the whole module in the app module.

So, let’s create a new file named: src/entries/entries.module.ts

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

import { Entry } from './entry.entity';
import { EntriesController } from './entry.controller';
import { EntriesService } from './entry.service';

@Module({
  imports: [TypeOrmModule.forFeature([Entry])],
  controllers: [EntriesController],
  components: [EntriesService],
})
export class EntriesModule {}

Remember when we included the TypeOrmModule in the AppModule as one of the first steps of this chapter? We used the TypeOrmModule.forRoot() formula there. However, we are using a different one here: TypeOrmModule.forFeature().

This distinction coming from the Nest.js TypeORM implementation allows us to separate different functionalities (“features”) in different modules. This way you can adapt your code to some of the ideas and best practices exposed in the Architecture chapter of this book.

Anyway, let’s import the new EntriesModule into the AppModule. If you neglect this step, your main app module won’t be aware of the existence of the EntriesModule and your app will not work as intended.

src/app.module.ts

import { TypeOrmModule } from '@nestjs/typeorm';
import { EntriesModule } from './entries/entries.module';

@Module({
  imports: [
    TypeOrmModule.forRoot(),
    EntriesModule,
    ...
  ]
})

export class AppModule {}

That’s it! Now you can fire requests to /entities and the endpoint will invoke writes and reads from the database.

It’s time to give our database a try! We will fire some requests to the endpoints that we previously linked to the database and see if everything works as expected.

We will start with a GET request to the /entries endpoint. Obviously, since we haven’t created any entries yet, we should receive an empty array as a response.

> GET /entries HTTP/1.1
> Host: localhost:3000
< HTTP/1.1 200 OK

[]

Let’s create a new entry.

> GET /entries HTTP/1.1
> Host: localhost:3000
| {
|   "id": 1,
|   "title": "This is our first post",
|   "body": "Bla bla bla bla bla",
|   "image": "http://lorempixel.com/400",
|   "created_at": "2018-04-15T17:42:13.911Z"
| }

< HTTP/1.1 201 Created

Success! Let’s retrieve the new entry by ID.

> GET /entries/1 HTTP/1.1
> Host: localhost:3000
< HTTP/1.1 200 OK

{
  "id": 1,
  "title": "This is our first post",
  "body": "Bla bla bla bla bla",
  "image": "http://lorempixel.com/400",
  "created_at": "2018-04-15T17:42:13.911Z"
}

Yes! Our previous POST request triggered a write in the database and now this last GET request is triggering a read from the database, and returning the data previously saved!

Let’s try to retrieve all entries once again.

> GET /entries HTTP/1.1
> Host: localhost:3000
< HTTP/1.1 200 OK

[{
  "id": 1,
  "title": "This is our first post",
  "body": "Bla bla bla bla bla",
  "image": "http://lorempixel.com/400",
  "created_at": "2018-04-15T17:42:13.911Z"
}]

We just confirmed that requests to the /entries endpoint successfully executed reads and writes in our database. This means that our Nest.js app is usable now, since the basic functionality of almost any server application (that is, storing data and retrieving it on demand) is working properly.

Improving our models

Even though we are now reading from and writing to the database through our entity, we only wrote a basic, initial implementation; we should review our code to see what can be improved.

Let’s now go back to the entity file, src/entries/entry.entity.ts, and figure out what kind of improvements we can do there.

Auto-generated IDs

All of the database entries need to have a unique ID. At this point, we are simply relying on the ID sent by the client when creating the entity (when sending the POST request,) but this is less than desirable.

Any server-side application will be connected to multiple clients, and all of those clients have no way of knowing which ID’s are already in use, so it would be impossible for them to generate and send a unique ID with each POST request.

TypeORM provides a couple of ways of generating unique ID’s for entities. The first one is using the @PrimaryGeneratedColumn() decorator. By using it, you no longer need to include an ID in the body of the POST request, nor do you need to manually generate an ID for an entry before saving it. Instead, the ID is automagically generated by TypeORM whenever you ask for a new entry to be saved to the database.

Our code looks something like the following:

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Entry {
  @PrimaryGeneratedColumn() id: number;

  @Column() title: string;

  @Column() body: string;

  @Column() image: string;

  @Column() created_at: Date;
}

It’s worth mentioning that these unique ID’s will be generated in a sequential way, which means that each ID will be one number higher than the highest already present in the database (the exact method for generating the new ID will depend on the database type.)

TypeORM can go one step further, though: if you pass the "uuid" argument to the @PrimaryGeneratedColumn() decorator, the generated value will then look like a random collection of letters and numbers with some dashes, making sure they’re unique (at least reasonably unique.)

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Entry {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Column() title: string;

  @Column() body: string;

  @Column() image: string;

  @Column() created_at: Date;
}

Also, remember to change the type of id from number to string!

When was the entry created?

In the original entity definition, the created_at field was also expected to be received from the client. We can, however, improve this easily with some more TypeORM magic decorators.

Let’s use the @CreateDateColumn() decorator to dynamically generate the insertion date for each entry. In other words, you don’t need to set the date from the client or create it manually before saving the entry.

Let’s update the entity:

import {
  Entity,
  Column,
  CreateDateColumn,
  PrimaryGeneratedColumn,
} from 'typeorm';

@Entity()
export class Entry {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Column() title: string;

  @Column() body: string;

  @Column() image: string;

  @CreateDateColumn() created_at: Date;
}

Nice, isn’t it? How about knowing also when the entry was last modified, as well as how many revisions have been done to it? Again, TypeORM makes both easy to do, and requires no additional code on our side.

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

@Entity()
export class Entry {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Column() title: string;

  @Column() body: string;

  @Column() image: string;

  @CreateDateColumn() created_at: Date;

  @UpdateDateColumn() modified_at: Date;

  @VersionColumn() revision: number;
}

Our entity will now automagically handle for us the modification date, as well as the revision number, on each subsequent save operations. You can track changes made to each instance of the entity without having to implement a single line of code!

Column types

When defining columns in our entities using decorators, as exposed above, TypeORM will infer the type of database column from the used property type. This basically means that when TypeORM finds a line like the following

@Column() title: string;

This maps the string property type to a varchar database column type.

This will work just fine a lot of the time, but in some occasions we might find ourselves in the position of being more explicit about the type of columns to be created in the database. Fortunately, TypeORM allows this kind of custom behavior with very little overhead.

To customize the column type, pass the desired type as a string argument to the @Column() decorator. A specific example would be:

@Column('text') body: string;

The exact column types that can be used depend on the type of database you are using.

Column types for mysql / mariadb

int, tinyint, smallint, mediumint, bigint, float, double, dec, decimal, numeric, date, datetime, timestamp, time, year, char, varchar, nvarchar, text, tinytext, mediumtext, blob, longtext, tinyblob, mediumblob, longblob, enum, json, binary, geometry, point, linestring, polygon, multipoint, multilinestring, multipolygon, geometrycollection

Column types for postgres

int, int2, int4, int8, smallint, integer, bigint, decimal, numeric, real, float, float4, float8, double precision, money, character varying, varchar, character, char, text, citext, hstore, bytea, bit, varbit, bit varying, timetz, timestamptz, timestamp, timestamp without time zone, timestamp with time zone, date, time, time without time zone, time with time zone, interval, bool, boolean, enum, point, line, lseg, box, path, polygon, circle, cidr, inet, macaddr, tsvector, tsquery, uuid, xml, json, jsonb, int4range, int8range, numrange, tsrange, tstzrange, daterange

Column types for sqlite / cordova / react-native

int, int2, int8, integer, tinyint, smallint, mediumint, bigint, decimal, numeric, float, double, real, double precision, datetime, varying character, character, native character, varchar, nchar, nvarchar2, unsigned big int, boolean, blob, text, clob, date

Column types for mssql

int, bigint, bit, decimal, money, numeric, smallint, smallmoney, tinyint, float, real, date, datetime2, datetime, datetimeoffset, smalldatetime, time, char, varchar, text, nchar, nvarchar, ntext, binary, image, varbinary, hierarchyid, sql_variant, timestamp, uniqueidentifier, xml, geometry, geography

Column types for oracle

char, nchar, nvarchar2, varchar2, long, raw, long raw, number, numeric, float, dec, decimal, integer, int, smallint, real, double precision, date, timestamp, timestamp with time zone, timestamp with local time zone, interval year to month, interval day to second, bfile, blob, clob, nclob, rowid, urowid

If you’re not ready to commit yourself to one specific database type and you’d like to keep your options open for the future, it might not be the best idea to use a type that’s not available in every database.

NoSQL in SQL

TypeORM has still one last trick in the hat: a simple-json column type that can be used in every supported database. With it, you can directly save Plain Old JavaScript Objects in one of the relational database columns. Yes, mindblowing!

Let’s put it to use with a new author property in the entity.

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

@Entity()
export class Entry {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Column() title: string;

  @Column('text') body: string;

  @Column() image: string;

  @Column('simple-json') author: { first_name: string; last_name: string };

  @CreateDateColumn() created_at: Date;

  @UpdateDateColumn() modified_at: Date;

  @VersionColumn() revision: number;
}

The simple-json column type allows you to directly store even complex JSON trees without needing to define a model for them first. This can come handy in situations where you appreciate a bit more flexibility than the traditional relational database structure allows.

Relationships between data models

If you followed the chapter up to this point, you will have a way of saving new blog entries to your database through your API and then reading them back.

The next step is to create a second entity to handle comments in each blog entry and then create a relationship between entries and comments in such a way that one blog entry can have several comments that belong to it.

Let’s create the Comments entity then.

src/comments/comment.entity.ts

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

@Entity()
export class Comment {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Column('text') body: string;

  @Column('simple-json') author: { first_name: string; last_name: string };

  @CreateDateColumn() created_at: Date;

  @UpdateDateColumn() modified_at: Date;

  @VersionColumn() revision: number;
}

You have probably noticed that the Comment entity is quite similar to the Entry entity.

The next step will be to create a “one-to-many” relationship between entries and comments. For that, include a new property in the Entry entity with a @OneToMany() decorator.

src/entries/entry.entity.ts

import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  VersionColumn,
  OneToMany,
} from 'typeorm';

import { Comment } from '../comments/comment.entity';

@Entity()
export class Entry {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Column() title: string;

  @Column('text') body: string;

  @Column() image: string;

  @Column('simple-json') author: { first_name: string; last_name: string };

  @OneToMany(type => Comment, comment => comment.id)
  comments: Comment[];

  @CreateDateColumn() created_at: Date;

  @UpdateDateColumn() modified_at: Date;

  @VersionColumn() revision: number;
}

“One-to-many” relationships have to be bi-directional, so you need to add an inverse relationship “many-to-one” in the Comment entity. This way, both will get properly “tied up.”

src/comments/comment.entity.ts

import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  VersionColumn,
  ManyToOne,
} from 'typeorm';

import { Entry } from '../entries/entry.entity';

@Entity()
export class Comment {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Column('text') body: string;

  @Column('simple-json') author: { first_name: string; last_name: string };

  @ManyToOne(type => Entry, entry => entry.comments)
  entry: Entry;

  @CreateDateColumn() created_at: Date;

  @UpdateDateColumn() modified_at: Date;

  @VersionColumn() revision: number;
}

The second argument that we’re passing to both the @OneToMany() and the @ManyToOne() decorators is used to specify the inverse relationship that we’re also creating on the other related entity. In other words, in the Entry we are saving the related Comment entity in a property named comments. That’s why, in the Comment entity definition, we pass entry => entry.comments as a second argument to the decorator, to the point where in Entry the comments be stored.

NOTE: Not all relationships need to be bi-directional. “One-to-one” relationships can very well be both uni-directional or bi-directional. In the case of uni-directional “one-to-one” relationships, the owner of the relationship is the one declaring it, and the other entity wouldn’t need to know anything about the first one.

That’s it! Now each of our entries can have several comments.

How to store related entities

If we talk about code, the most straightforward way of saving a comment that belongs to an entry would be to save the comment and then save the entry with the new comment included. Create a new Comments service to interact with the entity, and then modify the Entry controller to call that new Comments service.

Let’s see how. It’s not as hard as it sounds!

This would be our new service:

src/comments/comments.service.ts

import { Component } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { Comment } from './comment.entity';

@Component()
export class CommentsService {
  constructor(
    @InjectRepository(Comment) private readonly comment: Repository<Comment>
  ) {}

  findAll() {
    return this.comment.find();
  }

  findOneById(id: number) {
    return this.comment.findOneById(id);
  }

  create(comment: Comment) {
    return this.comment.save(comment);
  }
}

The code sure looks familiar, doesn’t it? It’s very similar to the EntriesService that we already had, since we are providing quite the same functionality for both comments and entries.

This would be the modified Entries controller:

src/entries/entries.controller.ts

import { Controller, Get, Post, Body, Param } from '@nestjs/common';

import { EntriesService } from './entries.service';
import { CommentsService } from '../comments/comments.service';

import { Entry } from './entry.entity';
import { Comment } from '../comments/comment.entity';

@Controller('entries')
export class EntriesController {
  constructor(
    private readonly entriesSrv: EntriesService,
    private readonly commentsSrv: CommentsService
  ) {}

  @Get()
  findAll() {
    return this.entriesSrv.findAll();
  }

  @Get(':entryId')
  findOneById(@Param('entryId') entryId) {
    return this.entriesSrv.findOneById(entryId);
  }

  @Post()
  async create(@Body() input: { entry: Entry; comments: Comment[] }) {
    const { entry, comments } = input;
    entry.comments: Comment[] = [];
    await comments.forEach(async comment => {
      await this.commentsSrv.create(comment);
      entry.comments.push(comment);
    });
    return this.entriesSrv.create(entry);
  }
}

In short, the new create() method:

  • Receives both a blog entry and an array of comments that belong to that entry.
  • Creates a new empty array property (named comments) inside the blog entry object.
  • Iterates over the received comments, saving each one of them and then pushing them one by one to the new comments property of entry.
  • Finally, saves the entry, which now includes a “link” to each comment inside its own comments property.

Saving related entities the easier way

The code we wrote last works, but it’s not very convenient.

Fortunately, TypeORM provides us with a easier way to save related entities, though: enabling “cascades”.

Setting cascade to true in our entity will mean that we’ll no longer need to separately save each related entity; rather, saving the owner of the relationship to the database will save those related entities at the same time. This way, our previous code can be simplified.

First of all, let’s modify our Entry entity (which is the owner of the relationship) to enable cascade.

src/entries/entry.entity.ts

import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  VersionColumn,
  OneToMany,
} from 'typeorm';

import { Comment } from '../comments/comment.entity';

@Entity()
export class Entry {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Column() title: string;

  @Column('text') body: string;

  @Column() image: string;

  @Column('simple-json') author: { first_name: string; last_name: string };

  @OneToMany(type => Comment, comment => comment.id, {
    cascade: true,
  })
  comments: Comment[];

  @CreateDateColumn() created_at: Date;

  @UpdateDateColumn() modified_at: Date;

  @VersionColumn() revision: number;
}

This was really easy: we just added a {cascade: true} object as third argument for the @OneToMany() decorator.

Now, we will refactor the create() method on the Entries controller.

src/entries/entries.controller.ts

import { Controller, Get, Post, Body, Param } from '@nestjs/common';

import { EntriesService } from './entries.service';

import { Entry } from './entry.entity';
import { Comment } from '../comments/comment.entity';

@Controller('entries')
export class EntriesController {
  constructor(private readonly entriesSrv: EntriesService) {}

  @Get()
  findAll() {
    return this.entriesSrv.findAll();
  }

  @Get(':entryId')
  findAll(@Param('entryId') entryId) {
    return this.entriesSrv.findOneById(entryId);
  }

  @Post()
  async create(@Body() input: { entry: Entry; comments: Comment[] }) {
    const { entry, comments } = input;
    entry.comments = comments;
    return this.entriesSrv.create(entry);
  }
}

Please compare the new controller with our previous implementations; we were able to get rid of the dependency on the Comments service, as well as an iterator on the create() method. This makes our code shorter and cleaner, which is always good as it reduces the risk of introducing bugs.

In this section we found out how to save entities that are related one to another, while saving their relationship as well. This is a crucial step for the success of our related entities. Nice job!

Retrieving related entities in bulk

Now that we know how to save an entity and include its relationships, we’ll take a look on how to read both an entity from the database, as well as all their related entities.

The idea in this case is that, when we request a blog entry (only one) from the database, we also get the comments that belong to it.

Of course, since you’re familiar with blogs in general (they’ve been around for a while, right?), you will be aware that not all blogs load both the blog entry and the comments at the same time; many of them load the comments only when you reach the bottom of the page.

To demonstrate the functionality, however, we will assume that our blogging platform will retrieve both the blog entry and the comments at the same time.

We will need to modify the Entries service to achieve this. Again, it’s going to be quite easy!

src/entries/entries.service.ts

import { Component } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { Entry } from './entry.entity';

@Component()
export class EntriesService {
  constructor(
    @InjectRepository(Entry) private readonly entry: Repository<Entry>
  ) {}

  findAll() {
    return this.entry.find();
  }

  findOneById(id: number) {
    return this.entry.findOneById(id, { relations: ['comments'] });
  }

  create(newEntry: Entry) {
    this.entry.save(newEntry);
  }
}

We only added a { relations: ['comments'] } as second argument to the findOneById() method of the Entry repository. The relations property of the options object is an array, so we can retrieve as many relationships we need to. Also, it can be used with any find() related method (that is, find(), findByIds(), findOne() and so on.)

Lazy relationships

When working with TypeORM, regular relationships (like the ones we have written so far) are eager relationships. This means that when we read entities from the database, the find*() methods will return the related entities as well, without us needing to write joins or manually read them.

We can also configure our entities to treat relationships as lazy, so that the related entities are not retrieved from the database until we say so.

This is achieved by declaring the type of the field that holds the related entity as a Promise instead of a direct type. Let’s see the difference in code:

// This relationship will be treated as eager
@OneToMany(type => Comment, comment => comment.id)
comments: Comment[];

// This relationship will be treated as lazy
@OneToMany(type => Comment, comment => comment.id)
comments: Promise<Comment[]>;

Of course, using lazy relationships means that we need to change the way we save our entity to the database. The next code block demonstrates how to save lazy relationships. Pay attention to the create() method.

src/entries/entries.controller.ts

import { Controller, Get, Post, Body, Param } from '@nestjs/common';

import { EntriesService } from './entries.service';
import { CommentsService } from '../comments/comments.service';

import { Entry } from './entry.entity';
import { Comment } from '../comments/comment.entity';

@Controller('entries')
export class EntriesController {
  constructor(
    private readonly entriesSrv: EntriesService,
    private readonly commentsSrv: CommentsService
  ) {}

  @Get()
  findAll() {
    return this.entriesSrv.findAll();
  }

  @Get(':entryId')
  findAll(@Param('entryId') entryId) {
    return this.entriesSrv.findOneById(entryId);
  }

  @Post()
  async create(@Body() input: { entry: Entry; comments: Comment[] }) {
    const { entry, comments } = input;
    const resolvedComments = [];
    await comments.forEach(async comment => {
      await this.commentsSrv.create(comment);
      resolvedComments.push(comment);
    });
    entry.comments = Promise.resolve(resolvedComments);
    return this.entriesSrv.create(entry);
  }
}

We made the create() method “lazy” by:

  1. Initializing a new resolvedComments empty array.
  2. Going through all of the comments received in the request, saving each one and then adding it to the resolvedComments array.
  3. When all comments are saved, we assign a promise to the comments property of entry, and then immediately resolve it with the array of comments built in step 2.
  4. Save the entry with the related comments as an already resolved promise.

The concept of assigning an immediately resolved promise as value of an entity before saving is not easy to digest. Still, we need to resort to this because of the asynchronous nature of JavaScript.

That said, be aware that TypeORM support for lazy relationships is still in the experimental phase, so use them with care.

Other kinds of relationships

So far we’ve explored “one-to-many” relationships. Obviously, TypeORM supports “one-to-one” and “many-to-many” relationships as well.

One-to-one

Just in case you’re not familiar with this kind of relationships, the idea behind it is that one instance of an entity, and only one, belongs to one instance, and only one, of another entity.

To give a more specific example, let’s imagine that we were going to create a new EntryMetadata entity to store new things that we want to keep track of, like, let’s say, the number of likes a blog entry got from readers and a shortlink for each blog entry.

Let’s start by creating a new entity called EntryMetadata. We will put the file in the /entry folder, next to the entry.entity.ts file.

src/entries/entry_metadata.entity.ts

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class EntryMetadata {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Column() likes: number;

  @Column() shortlink: string;
}

The entity we just created is quite simple: it only has the regular uuid property, as well as two other properties for storing likes for an entry, and also a shortlink for it.

Now let’s tell TypeORM to include one instance of the EntryMetadata entity in each instance of the Entry entity.

src/entries/entry.entity.ts

import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  VersionColumn,
  OneToMany,
  OneToOne,
  JoinColumn,
} from 'typeorm';

import { EntryMetadata } from './entry-metadata.entity';
import { Comment } from '../comments/comment.entity';

@Entity()
export class Entry {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Column() title: string;

  @Column('text') body: string;

  @Column() image: string;

  @Column('simple-json') author: { first_name: string; last_name: string };

  @OneToOne(type => EntryMetadata)
  @JoinColumn()
  metadata: EntryMetadata;

  @OneToMany(type => Comment, comment => comment.id, {
    cascade: true,
  })
  comments: Comment[];

  @CreateDateColumn() created_at: Date;

  @UpdateDateColumn() modified_at: Date;

  @VersionColumn() revision: number;
}

You might have noticed the @JoinColumn() decorator. Using this decorator in “one-to-one” relationships is required by TypeORM.

Bi-directional one-to-one relationships

At this point, the relationship between Entry and EntryMetadata is uni-directional. In this case, it is probably enough.

Let’s say, however, that we want to have the possibility of accessing an instance of EntryMetadata directly and then fetch the Entry instance it belongs to. Well, we can’t do that right now; not until we make the relationship bi-directional.

So, just for the sake of demonstration, we will include the inverse relationship in the EntryMetadata instance to the Entry instance, so that you know how it works.

src/entries/entry_metadata.entity.ts

import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm';

import { Entry } from './entry.entity';

@Entity()
export class EntryMetadata {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Column() likes: number;

  @Column() shortlink: string;

  @OneToOne(type => Entry, entry => entry.metadata)
  entry: Entry;
}

Make sure you don’t include the @JoinColumn() decorator on this second entry. That decorator should only be used in the owner entity; in our case, in Entry.

The second adjustment we need to make is pointing to the location of the related entity in our original @OneToOne() decorator. Remember, we just saw that this needs to be done by passing a second argument to the decorator, like this:

src/entries/entry.entity.ts

import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
  VersionColumn,
  OneToMany,
  OneToOne,
  JoinColumn,
} from 'typeorm';

import { EntryMetadata } from './entry-metadata.entity';
import { Comment } from '../comments/comment.entity';

@Entity()
export class Entry {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Column() title: string;

  @Column('text') body: string;

  @Column() image: string;

  @Column('simple-json') author: { first_name: string; last_name: string };

  @OneToOne(type => EntryMetadata, entryMetadata => entryMetadata.entry)
  @JoinColumn()
  metadata: EntryMetadata;

  @OneToMany(type => Comment, comment => comment.id, {
    cascade: true,
  })
  comments: Comment[];

  @CreateDateColumn() created_at: Date;

  @UpdateDateColumn() modified_at: Date;

  @VersionColumn() revision: number;
}

That’s it! Now we have a beautiful, working bi-directional one-to-one relationship between the Entry and the EntryMetadata entities.

By the way, if you’re wondering how could we save and then retrieve this two related entities, I’ve got good news for you: it works the same way that we saw with one-to-many relationships. So, either do it by hand as exposed earlier in this chapter, or (my personal favorite) use “cascades” for saving them, and find*() to retrieve them!

Many-to-many

The last type of relationship that we can establish for our entities is known as “many-to-many.” This means that multiple instances of the owning entity can include multiple instances of the owned entity.

A good example might be us wanting to include “tags” to our blog entries. An entry might have several tags, and a tag can be used in several blog entries, right. That makes the relationship fall under the “many-to-many” typology.

We will save some code here, because these relationships are declared exactly the same way than the “one-to-one” relationships, only changing the @OneToOne() decorator to @ManyToMany().

Advanced TypeORM

Let’s take a look at security.

Security first

If you went through the Sequelize chapter in this same book, you might be familiar with the concept of lifecycle hooks. In that chapter, we are using a beforeCreate hook to encrypt the users’ passwords before we save them to our database.

In case you’re wondering if such a thing exists also in TypeORM, the answer is yes! Though the TypeORM documentation refers to them as “listeners” instead.

So, to demonstrate its functionality, let’s write a very simple User entity with a username and a password, and we will make sure to encrypt the password before we save it to the database. The specific listener we will be using is called beforeInsert in TypeORM.

@Entity
export class User {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Column() username: string;

  @Column() password: string;

  @BeforeInsert()
  encryptPassword() {
    this.password = crypto.createHmac('sha256', this.password).digest('hex');
  }
}

Other listeners

In general, a listener is a method that gets triggered upon a specific event within TypeORM, be it write-related or read-related. We just learned about the @BeforeInsert() listener, but we have a few other ones we can take advantage of:

  • @AfterLoad()
  • @BeforeInsert()
  • @AfterInsert()
  • @BeforeUpdate()
  • @AfterUpdate()
  • @BeforeRemove()
  • @AfterRemove()

Composing and extending entities

TypeORM offers two different ways of reducing code duplication between entities. One of them follows the composition pattern, while the other follows the inheritance pattern.

Even though a lot of authors defend favoring composition over inheritance, we will expose here the two possibilities and let the reader decide which one fits better his/her own particular needs.

Embedded entities

The way of composing entities in TypeORM is using an artifact known as embedded entity.

Embedded entities are basically entities with some declared table columns (properties) that can be included inside other bigger entities.

Let’s go with the example: after reviewing the code we wrote earlier for the entities of both Entry and Comment, we can easily see that there are (among others) three duplicated properties: created_at, modified_at and revision.

It would be a great idea to create an “embeddable” entity to hold those three properties and then embed them into both our original entities. Let’s see how.

We will first create a Versioning entity (the name is not great, I know, but should work for you to see the idea) with those three duplicated properties.

src/common/versioning.entity.ts

import { CreateDateColumn, UpdateDateColumn, VersionColumn } from 'typeorm';

export class Versioning {
  @CreateDateColumn() created_at: Date;

  @UpdateDateColumn() modified_at: Date;

  @VersionColumn() revision: number;
}

Notice that we’re not using the @Entity decorator in this entity. This is because it’s not a “real” entity. Think of it as an “abstract” entity, i.e. an entity that we will never instantiate directly, but we rather will use to embed it in other instantiable entities in order to give them some reusable functionality. Or, in other words, composing entities from smaller pieces.

So, now we will embed this new “embeddable” entity into our two original entities.

src/entries/entry.entity.ts

import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  OneToMany,
  OneToOne,
  JoinColumn,
} from 'typeorm';

import { EntryMetadata } from './entry-metadata.entity';
import { Comment } from '../comments/comment.entity';
import { Versioning } from '../common/versioning.entity';

@Entity()
export class Entry {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Column() title: string;

  @Column('text') body: string;

  @Column() image: string;

  @Column('simple-json') author: { first_name: string; last_name: string };

  @OneToOne(type => EntryMetadata, entryMetadata => entryMetadata.entry)
  @JoinColumn()
  metadata: EntryMetadata;

  @OneToMany(type => Comment, comment => comment.id, {
    cascade: true,
  })
  comments: Comment[];

  @Column(type => Versioning)
  versioning: Versioning;
}

src/comments/comment.entity.ts

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

import { Versioning } from '../common/versioning.entity';

@Entity()
export class Comment {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Column('text') body: string;

  @Column('simple-json') author: { first_name: string; last_name: string };

  @Column(type => Versioning)
  versioning: Versioning;
}

Even in this really simple case, we’ve reduced the two original entities from three different properties to only one! In both the Entry entity and the Comment entity, the versioning column will be actually replaced by the properties inside the Versioning embedded entity when we invoke any of their reading or writing methods.

Entity inheritance

The second choice that TypeORM offers for reusing code between our entities is using entity inheritance.

If you’re already familiar with TypeScript, entity inheritance is quite easy to understand (and implement) when you take into account that entities are nothing more (and nothing less!) than regular TS classes with some decorators on top.

For this particular example, let’s imagine that our Nest.js-based blog has been online for some time, and that it has become quite a success. Now we would like to introduce sponsored blog entries so that we can make a few bucks and invest them in a few more books.

The thing is, sponsored entries are going to be a lot like regular entries, but with a couple of new properties: sponsor name and sponsor URL.

In this case, we might decide, after quite some thought, to extend our original Entry entity and create a SponsoredEntry out of it.

src/entries/sponsored-entry.entity.ts

import { Entity, Column } from 'typeorm';

import { Entry } from './entry.entity';

@Entity()
export class SponsoredEntry extends Entry {
  @Column() sponsorName: string;

  @Column() sponsorUrl: string;
}

That’s about it. Any new instance we create from the SponsoredEntry entity will have the same columns from the extended Entry entity, plus the two new columns we defined for SponsoredEntry.

Caching

TypeORM brings a caching layer out of the box. We can take advantage of it with only a little overhead. This layer is specially useful if you are designing an API that expects a lot of traffic and/or you need the best performance you can get.

Both cases would benefit increasingly from the cache because we use more complex data retrieval scenarios, such as complex find*() options, lots of related entities, etc.

The caching needs to be explicitly activated when connecting to the database. In our case so far, this will be the ormconfig.json file that we created at the beginning of the chapter.

ormconfig.json

{
  "type": "mariadb",
  "host": "db",
  "port": 3306,
  "username": "nest",
  "password": "nest",
  "database": "nestbook",
  "synchronize": true,
  "entities": ["src/**/*.entity.ts"],
  "cache": true
}

After activating the caching layer on the connection, we will need to pass the cache option to our find*() methods, like in the following example:

this.entry.find({ cache: true });

The line of code above will make the .find() method to return the cached value if it’s present and not expired, or the value from the corresponding database table otherwise. So, even if the method is fired three thousand times within the expiration time window (see below), only one database query would be actually executed.

TypeORM uses a couple of defaults when dealing with caches:

  1. The default cache lifetime is 1,000 milliseconds (i.e. 1 second.) In case we need to customize the expiration time, we just need to pass the desired lifetime as value to the cache property of the options object. In the case above, this.entry.find({ cache: 60000 }) would set a cache TTL of 60 seconds.
  2. TypeORM will create a dedicated table for the cache in the same database you’re already using. The table will be named query-result-cache. This is not bad, but it can greatly improved if we have a Redis instance available. In that cache, we will need to include our Redis connection details in the ormconfig.json file:

ormconfig.json

{
  "type": "mariadb",
  ...
  "cache": {
    "type": "redis",
    "options": {
      "host": "localhost",
      "port": 6379
    }
  }
}

This way we can easily improve the performance of our API under heavy load.

Building a query

The TypeORM’s repository methods for retrieving data from our database greatly isolates the complexity of querying away from us. They provide a very useful abstraction so that we don’t need to bother with actual database queries.

However, apart from using these various .find*() methods, TypeORM also provides a way of manually executing queries. This greatly improves flexibility when accessing our data, at the cost of demanding us to write more code.

The TypeORM tool for executing queries is the QueryBuilder. A very basic example could involve refactoring our good old findOneById() method in the EntriesService so that it uses the QueryBuilder.

src/entries/entries.service.ts

import {getRepository} from "typeorm";
...

findOneById(id: number) {
  return getRepository(Entry)
    .createQueryBuilder('entry')
    .where('entry.id = :id', { id })
    .getOne();
}

...

Another slightly more complex scenario would be to build a join in order to also retrieve the related entities. We will come back once again to the findOneById() method we just modified to include the related comments.

src/entries/entries.service.ts

import {getRepository} from "typeorm";
...

findOneById(id: number) {
  return getRepository(Entry)
    .createQueryBuilder('entry')
    .where('entry.id = :id', { id })
    .leftJoinAndSelect('entry.comments', 'comment')
    .getOne();
}

...

Building our model from a existing database

Up until this point, we have started with a “clean” database, then created our models, leaving to TypeORM the task of transforming the models into database columns.

This is the “ideal” situation, but... What if we found ourselves in the opposite situations? What if we already had a database with tables and columns populated?

There’s a nice open source project we can use for that: typeorm-model-generator. It’s packed as a command line tool and can be run with npx.

NOTE: In case you’re not familiar with it, npx is a command that comes out of the box with npm > 5.2 and that allows us to run npm modules from the command line without having to install them first. To use it, you just need to prepend npx to the regular commands from the tool. We would use npx ng new PROJECT-NAME on our command line, for example, if we wanted to scaffold a new project with Angular CLI.

When it’s executed, typeorm-model-generator will connect to the specified database (it supports roughly the same ones that TypeORM does) and will generate entities following the settings we pass as command line arguments.

Since this is a useful tool for only some very specific use cases, we will leave the configuration details out of this book. However, if you find yourself using this tool, go ahead and check its GitHub repository.

Summary

TypeORM is a very useful tool and enables us to do a lot of heavy lifting when dealing with databases, while greatly abstracting things like data modelling, queries, and complex joins, thus simplifying our code.

It’s also very suitable for being used in Nest.js-based projects thanks to the great support the framework provides through the @nest/typeorm package.

Some of the things that we covered in this chapter are:

  • The database types supported by TypeORM and some hints on how to choose one.
  • How to connect TypeORM to your database.
  • What is an entity and how to create your first one.
  • Storing and retrieving data from your database.
  • Leveraging TypeORM to make it easier working with metadata (ID’s, creation and modification dates...).
  • Customizing the type of columns in your database to match your needs.
  • Building relationships between your different entities and how to handle them when reading from and writing to the database.
  • More advanced procedures like reusing code through composition or inheritance; hooking into lifecycle events; caching; and building queries by hand.

All in all, we really think the more familiar you grow with Nest.js, the more likely you start to feel comfortable writing TypeORM code, since they both look alike in a few aspects as their extensive use of TypeScript decorators.

In the next chapter we cover Sequelize, which is a promise-based ORM.