How to setup TypeORM migrations in a NestJS project

How to setup TypeORM migrations in a NestJS project

Complete database migration setup from scratch in a NestJS project using TypeORM

Motivation

TypeORM is the most mature Object Relational Mapper (ORM) for TypeScript and JavaScript. Since it's written in TypeScript, it integrates well with the NestJS framework. For integrating with SQL and NoSQL databases, Nest provides the @nestjs/typeorm package. Here the official documentation of NestJS has clear instructions on how to integrate TypeORM with it. However, they skipped the details on how to setup the migrations and forwarded them to the TypeORM documentation for migration, which is very generic and not aligned with NestJS. That is the motivation of this article. In this article, I'll show how to setup migrations in a NestJS project using TypeORM. Buckle up and let's get started.

Initial project setup

Let's initialize our project using Nest CLI.

nest new nest-typeorm

Navigate to the project folder and install the necessary dependencies. In this example, I am using PostgreSQL, therefore I have added pg as a dependency.

cd nest-typeorm
npm install --save @nestjs/typeorm typeorm pg

Database connection

Let's create a separate Nest module for handling database-related tasks.

nest g module db

Create the data-source.ts file inside the src/db directory. This file will contain database connection configurations.

// src/db/data-source.ts
import { config } from 'dotenv';
import { DataSourceOptions } from 'typeorm';

config();

export const dataSourceOptions: DataSourceOptions = {
  type: 'postgres',
  host: process.env.DB_HOST,
  port: +process.env.DB_PORT,
  username: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  entities: [],
};

Use this dataSourceOptions for setting up TypeOrmModule inside the DbModule that we generated using Nest CLI.

// src/db/db.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { dataSourceOptions } from './data-source';

@Module({
  imports: [TypeOrmModule.forRoot(dataSourceOptions)],
})
export class DbModule {}

Now if you run the project using npm run start:dev command, it should be able to connect to the database. However, you have to make sure the database server is up and running and the database with the name that we specified in the data-source file is already created.

Generate an entity

Generate users resource using the CLI.

nest g resource resource/users

It will generate the users module, controller, service, DTOs and entity file inside src/resource/users directory.

Let's update the user.entity.ts file and add some columns there.

// src/resources/users/entities/user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

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

  @Column({ name: 'first_name' })
  firstName: string;

  @Column({ name: 'last_name' })
  lastName: string;
}

Now we need to create the migration for creating the user table with the id, first_name, last_name columns.

Generate the migration

Migration in TypeORM is just a class that implements MigrationInterface from the typeorm package. It needs to implement the up and down methods. The up method executes the query for applying new changes in the database. On the other hand, down method executes the query to revert the changes that was applied by the up method.

It is possible to implement a migration by manually implementing the MigrationInterface. However, TypeORM provides a nice CLI to generate that automatically based on the data-source and entity.

Now the first thing is, we need to make TypeORM aware that we have an entity called user. To do that, we need to update the entities array in the data-source file. The entities array accepts both entity classes and directories from where entities need to be loaded. Directories support glob pattern. In our case, we want every entity from our resources directory to be loaded.

// src/db/data-source.ts
...
export const dataSourceOptions: DataSourceOptions = {
  ...
  entities: ['src/resources/**/*.entity.ts'],
};

Though we are working with typescript, the above code will throw Cannot use import statement outside a module error. To make it work smoothly it is safe to use .js files. Our build command will generate the javascript files during build time and put them into /dist directory. We can simply use that path here.

// src/db/data-source.ts
...
export const dataSourceOptions: DataSourceOptions = {
  ...
  entities: ['dist/resources/**/*.entity.js'],
};

At this point, our project is ready for generating the first migration. We will use TypeORM CLI to generate the migration file. The command for generating a migration file from a data-source is like this.

npx typeorm migration:generate -d path/to/datasource path/to/Migration

However, we are using entity path from dist directory. That means, we need to run the build command before using the typeorm command. We can combined those commands like this.

npm run build && npx typeorm migration:generate -d path/to/datasource path/to/Migration

Let's add a script in our package.json file combining these commands.

// package.json
{
  ...
  "scripts": {
    ...
    "migration:generate": "npm run build && npx typeorm migration:generate -d ./dist/db/data-source.js",
  }
}

Notice that, we have added the path to data-soure file in the script but we omitted the path to migration file. We want to pass that dynamically from the command line when we invoke the script.

Let's invoke the script.

npm run migration:generate src/db/migrations/create-user-table

Once we execute this command in the terminal it will generate the migration file in the src/db/migrations directory. The file name will be something like <timestamp>-create-user-table.ts

If you open the migration file you can see the code like below.

// src/db/migrations/<timestamp>-create-user-table.ts
import { MigrationInterface, QueryRunner } from "typeorm";

export class CreateUserTable1690462207991 implements MigrationInterface {
    name = 'CreateUserTable1690462207991'

    public async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`CREATE TABLE "user" ("id" SERIAL NOT NULL, "first_name" character varying NOT NULL, "last_name" character varying NOT NULL, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`);
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`DROP TABLE "user"`);
    }

}

Run the migration

The migration file is generated. Now we need to execute it to update our database structure.

Before using the migration:run command from TypeORM CLI we need to update the dataSourceOptions and add migrations array to tell TypeORM from where it should read the migration files.

// src/db/data-source.ts
...
export const dataSourceOptions: DataSourceOptions & SeederOptions = {
  ...
  migrations: ['dist/db/migrations/*.js'],
};

We have used the glob pattern to pick migration files from the dist directory (similar to entities array that we added before). Therefore, we must build the project before running the migration command. Same as before, we can combine the build and migration commands in the package.json file.

Let's add the script to run the migration in our package.json file.

// package.json
{
  ...
  "scripts": {
    ...
    "migration:run": "npm run build && npx typeorm migration:run -d ./dist/db/data-source.js",
  }
}

Run the script through the terminal.

npm run migration:run

If you check your database now, you can see the user table is created.

Conclusion

We have learned how to set up migration in a NestJS project using TypeORM.

You can find the full project here:
https://github.com/mazid1/nestjs-typeorm/tree/feature/migration

In the next article, I will write about how to seed test data using TypeORM.


I'm open for a remote full-time/contract/freelance job for the position of Full-stack Software Engineer. If you are hiring, ping me at