TypeORM Best Practices using Typescript and NestJS at Libeo

Fanny Huang
Tales of Libeo
Published in
5 min readNov 25, 2020
Photo by Richard Tao on Unsplash

We made mistakes in the early days of writing and building our platform Libeo, just as many startups have. To prevent us from making the same mistakes, we adjusted and redefined our coding patterns as we learned and progressed.

This article was originally an internal note built to standardize our codebase. Our goal is to make code easy to read and understandable for everyone including newcomers, junior developers, and senior developers. This article acts as a guide to prevent us from introducing bugs into our app or making the same coding mistake twice.

Database reading and writing can have a huge impact on app performance. We always try to keep this in mind while coding! 🤭

We want this guide of best practices to be able to help other teams with coding. Of course, each team is always evolving, but we want to make the process easier than ever.

We are happy to introduce Libeo’s tech team’s best coding practices using Typescript / NestJS and TypeORM! 🤗

Entities

Strongly Type Entities

Entity classes represent our database table schema. Files that define an entity must have the extension entity.ts.

Thanks to Typescript we can strongly type our entity’s class members and let developers know what exactly our database columns contain.

Can the loaded data return null? (i.e. nullable=false.) If the property is a relation, type it with the corresponding Entity.

For example let’s take the table column payload which in PostgreSQL is a simple-json, in typescript, we will declare a new type of interface with the JSON values.

Example:

Example of Strongly Typed Entity

Here, our invoice entity has an importedById column which is not nullable. The ManyToOne relation indicates that importedBy is of type User and its ID actually matches a foreign key. The property importedBy is only defined when the relationship is queried (or if eager is set) so we type it adding User | undefined.

Branded Entity IDs

More information here: https://basarat.gitbook.io/typescript/main-1/nominaltyping

We recently started to use nominal typing in our entity IDs. All of our database IDs are represented as a UUID string. By branding our IDs, they are no longer just a string but have a specific type that defines them. This way, we cannot provide an ID from another entity to a given entity e.g. as a class method that has arguments userId and companyId. If by some mistake companyId is given to userId and vice-versa, the code would not compile as the types do not match. Without a branded type, this small careless act would not be detected at compile-time and a bug could be introduced.

Example:

Example of Branded Entity ID

In user.service.ts

Example of Code Not Compiling (1)
Example of Code Not Compiling — Compilation Error(2)

BaseEntity Inheritance

Every entity should implement BaseEntity. This base contains our tables mandatory columns:

  • id : UUID v4, id of the row
  • created_at : timestamp, when row was created
  • updated_at: timestamp, when row was last updated
  • version : integer, number of times the row has been updated (default to 1)

TypeORM Data Mapper Pattern / Repository

Repository Methods Naming Convention

Method naming is extremely important. Be extra explicit when naming repository methods.

If a function should fail because the entity was not found, mention it. If it returns an entity with specific relations, mention it. If it just updates some specific fields, mention it.

Dependencies Injections and Repositories

There are multiple ways of using TypeORM repositories. You can instantiate using its queryRunner or its connection. Then :

Example of TypeORM connection declaration

But, we would rather declare repositories in class constructors by injecting it. First, add the repository in your module.ts :

Example Dependency Injection Using a Repository (1)

And then, in your service.ts:

Example Dependency Injection Using a Repository (2)

Custom Repository

Instead of injecting a new repository instance into our services, we define a new class that extends TypeORM’s repository and lists every method we need to use. Yup, every method.

Everything that concerns entity querying, saving, or other methods should be defined by the entity’s repository. It allows us to have a global view of how, what, and where transactions are made in the database.

To do this, indicate the use of your custom repository in modules and services:

Example Dependency Injection Using a Custom Repository

Relationship

Use of Eager

TypeORM eager option on a relation makes the entity’s instant load the relation every time it’s loaded from the database. It can be really useful, but it can also be extremely heavy during the database queries. Use it wisely!

If you remove the eager option for performance purposes, you will most likely have bugs in the code you already implemented as the relation will not be resolved anymore.

Loading Entity With Its Relations and Typesafe Relations

When an entity has a relation to another entity, we can load this relation using relations in TypeORM FindOptions. However, when defining an entity, the property should be defined as potentially undefined, as we do not load every time (see Strongly Type Entities for more informations).

Now we know that our entity’s instance is carrying the relation. To avoid checking if the relation exists each time it’s used in our code, we created a new interface extending the required entity and defined the relation as NonNullable. Later, we test our returned entity in a type-safe function so it can correctly infer the type.

If the property’s name is later refactored, Typescript is smart enough to rename all references regardless of whether the property is accessed using dot notation or bracket notation.

Example of Typesafing Entity Relation

Conclusion

At Libeo, we really care about code quality and that’s why we encourage our developers to follow this guide of best practices. We even re-challenge them every so often to keep up to date.

We are always eager to listen to new coding styles or coding patterns. What are your thoughts about ours? We’d love to hear your suggestions!

--

--