Skip to content

Setting Up TypeORM Migrations in an Nx/NestJS Project

TypeORM is a powerful Object-Relational Mapping (ORM) library for TypeScript and JavaScript that serves as an easy-to-use interface between an application's business logic and a database, providing an abstraction layer that is not tied to a particular database vendor. TypeORM is the recommended ORM for NestJS as both are written in TypeScript, and TypeORM is one of the most mature ORM frameworks available for TypeScript and JavaScript.

One of the key features of any ORM is handling database migrations, and TypeORM is no exception. A database migration is a way to keep the database schema in sync with the application's codebase. Whenever you update your codebase's persistence layer, perhaps you'll want the database schema to be updated as well, and you want a reliable way for all developers in your team to do the same with their local development databases.

In this blog post, we'll take a look at how you could implement database migrations in your development workflow if you use a NestJS project. Furthermore, we'll give you some ideas of how nx can help you as well, if you use NestJS in an nx-powered monorepo.

🔗Migrations Overview

In a nutshell, migrations in TypeORM are TypeScript classes that implement the MigrationInterface interface. This interface has two methods: up and down, where up is used to execute the migration, and down is used to rollback the migration. Assuming that you have an entity (class representing the table) as below:

import { Entity, Column, PrimaryGeneratedColumn } from "typeorm"

@Entity()
export class Post {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  title: string

  @Column()
  text: string
}

If you generate a migration from this entity, it could look as follows:

import { MigrationInterface, QueryRunner } from 'typeorm';

export class CreatePost1674827561606 implements MigrationInterface {
  name = 'CreatePost1674827561606';

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

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

As can be seen by the SQL commands, the up method will create the post table, while the down method will drop it. How do we generate the migration file, though? The recommended way is through the TypeORM CLI.

🔗TypeORM CLI and TypeScript

The CLI can be installed globally, by using npm i -g typeorm. It can also be used without installation by utilizing the npx command: npx typeorm <params>. The TypeORM CLI comes with several scripts that you can use, depending on the project you have, and whether the entities are in JavaScript or TypeScript, with ESM or CommonJS modules:

  • typeorm: for JavaScript entities
  • typeorm-ts-node-commonjs: for TypeScript entities using CommonJS
  • typeorm-ts-node-esm: for TypeScript entities using ESM

Many of the TypeORM CLI commands accept a data source file as a mandatory parameter. This file provides configuration for connecting to the database as well as other properties, such as the list of entities to process. The data source file should export an instance of DataSource, as shown in the below example:

// typeorm.config.ts

import { DataSource } from 'typeorm';
import { Post } from './models/post.entity';

export default new DataSource({
  type: 'postgres',
  host: process.env.DATABASE_HOST,
  port: parseInt(process.env.DATABASE_PORT as string),
  username: process.env.DATABASE_USERNAME,
  password: process.env.DATABASE_PASSWORD,
  database: process.env.DATABASE_NAME,
  entities: [Post],
});

To use this data source, you would need to provide its path through the -d argument to the TypeORM CLI. In a NestJS project using ESM, this would be:

typeorm-ts-node-esm -d src/typeorm.config.ts migration:generate CreatePost

If the DataSource did not import the Post entity from another file, this would most likely succeed. However, in our case, we would get an error saying that we "cannot use import statement outside a module". The typeorm-ts-node-esm script expects our project to be a module -- and any importing files need to be modules as well. To turn the Post entity file into a module, it would need to be named post.entity.mts to be treated as a module.

This kind of approach is not always preferable in NestJS projects, so one alternative is to transform our DataSource configuration to JavaScript - just like NestJS is transpiled to JavaScript through Webpack. The first step is the transpilation step:

tsc src/typeorm.config.ts --outDir "./dist"

Once transpiled, you can then use the regular typeorm CLI to generate a migration:

typeorm -d dist/typeorm.config.js migration:generate CreatePost

Both commands can be combined together in a package.json script:

// package.json

{
  "scripts": {
    "typeorm-generate-migrations": "tsc src/typeorm.config.ts --outDir ./dist && typeorm -d dist/typeorm.config.js migration:generate"
  }  
}

After the migrations are generated, you can use the migration:run command to run the generated migrations. Let's upgrade our package.json with that command:

// package.json

{
  "scripts": {
    "typeorm-build-config": "tsc src/typeorm.config.ts --outDir ./dist",
    "typeorm-generate-migrations": "npm run typeorm-build-config && typeorm -d dist/typeorm.config.js migration:generate",
    "typeorm-run-migrations": "npm run typeorm-build-config && typeorm -d dist/typeorm.config.js migration:run"
  }  
}

🔗Using Tasks in Nx

If your NestJS project is part of an nx monorepo, then you can utilize nx project tasks. The benefit of this is that nx will detect your tsconfig.json as well as inject any environment variables defined in the project. Assuming that your NestJS project is located in an app called api, the above npm scripts can be written as nx tasks as follows:

// apps/api/project.json
{
  // ...
  "targets": {
    "build-migration-config": {
      "executor": "@nrwl/node:webpack",
      "outputs": ["{options.outputPath}"],
      "options": {
        "outputPath": "dist/apps/typeorm-migration",
        "main": "apps/api/src/app/typeorm.config.ts",
        "tsConfig": "apps/api/tsconfig.app.json"
      }
    },
    "typeorm-generate-migrations": {
      "executor": "@nrwl/workspace:run-commands",
      "outputs": ["{options.outputPath}"],
      "options": {
        "cwd": "apps/api",
        "commands": ["typeorm -d ../../dist/apps/typeorm-migration/main.js migration:generate"]
      },
      "dependsOn": ["build-migration-config"]
    },
    "typeorm-run-migrations": {
      "executor": "@nrwl/workspace:run-commands",
      "outputs": ["{options.outputPath}"],
      "options": {
        "cwd": "apps/api",
        "commands": ["typeorm -d ../../dist/apps/typeorm-migration/main.js migration:run"]
      },
      "dependsOn": ["build-migration-config"]
    }
  },
  "tags": []
}

The typeorm-generate-migration and typeorm-run-migrations tasks depend on the build-migration-config task, meaning that they will always transpile the data source config first, before invoking the typeorm CLI.

For example, the previous CreatePost migration could be generated through the following command:

 nx run api:typeorm-generate-migration CreatePost

🔗Conclusion

TypeORM is an amazing ORM framework, but there are a few things you should be aware of when running migrations within a big TypeScript project like NestJS. We hope we managed to give you some tips on how to best incorporate migrations in an NestJS project, with and without nx.

This Dot Labs is a JavaScript consulting firm that enables companies to build and improve their digital technologies with confidence. For expert architectural guidance, training, consulting, engineering leadership, and development services in React, Angular, Vue, Web Components, GraphQL, Node, Bazel, Polymer, and more, visit thisdot.co

You might also like