TypeORM Best Practices using Typescript and NestJS at Libeo
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:
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:
In user.service.ts
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 :
But, we would rather declare repositories in class constructors by injecting it. First, add the repository in your module.ts
:
And then, in your service.ts
:
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:
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.
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!