Javascript

Setting Up TypeORM Migrations in an Nx/NestJS Project

Dario Djuric
4 min read
Setting Up TypeORM Migrations in an Nx/NestJS Project
This article was written over 18 months ago and may contain information that is out of date. Some content may still be relevant, but please refer to official documentation for the latest information.

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.

About the author

Dario Djuric

Dario Djuric

Senior Software Engineer

Dario is a full-stack engineer who has spent most of his career doing enterprise Java projects. He has always had a hidden passion for frontend, though -- and he is now able to pursue that passion at This Dot. He spends most of his free time with his two sons, but occasionally, he manages to squeeze in some casual sports activities such as jogging, cycling, and soccer.

Keep reading

View all posts →

Deploying Multiple Apps From a Monorepo to GitHub Pages

Explore deploying multiple front-end applications on GitHub Pages with our guide. Learn how to navigate the challenges of client-side routing and efficiently manage multiple apps in one repository....

Jan Kaiser4 mins
GitHubGitHub ActionsNxReact

How to automatically deploy your full-stack JavaScript app with AWS CodePipeline

In our previous blog post we set up a horizontally scalable deployment for our full-stack javascript app. In this article we would like to show you how to set up AWS CodePipeline to automatically deploy changes to the application....

Balázs Tápai9 mins
AWSNxNodeJSJavaScript

How to host a full-stack app with AWS CloudFront and Elastic Beanstalk

You have an SPA with a NestJS back-end. What if your app is a hit? You need to be prepared to serve thousands of users? You might need to scale your API horizontally, which means you need to have more instances running behind a load balancer. ...

Balázs Tápai12 mins
NestJSAWSTypeScriptNodeJS

Linting, Formatting, and Type Checking Commits in an Nx Monorepo with Husky and lint-staged

One way to keep your codebase clean is to enforce linting, formatting, and type checking on every commit. This is made very easy with pre commit hooks. Using Husky, you can run arbitrary commands before a commit is made. This can be combined with lint staged, which allows you to run commands on only the files that have been staged for commit. This is useful because you don't want to run linting, formatting, and type checking on every file in your project, but only on the ones that have been changed. But if you're using an Nx monorepo for your project, things can get a little more complicated. Rather than have you use eslint or prettier directly, Nx has its own scripts for linting and formatting. And type checking is complicated by the use of specific tsconfig.json files for each app or library. Setting up pre commit hooks with Nx isn't as straightforward as in a simpler repository. This guide will show you how to set up pre commit hooks to run linting, formatting, and type checking in an Nx monorepo. Configure Formatting Nx comes with a command, nx format:write for applying formatting to affected files which we can give directly to lint staged. This command uses Prettier under the hood, so it will abide by whatever rules you have in your root level .prettierrc file. Just install Prettier, and add your preferred configuration. Then add a .prettierrc file to the root of your project with your preferred configuration. For example, if you want to use single quotes and trailing commas, you can add the following: Configure Linting Nx has its own plugin that uses ESLint to lint projects in your monorepo. It also has a plugin with sensible ESLint defaults for your linter commands to use, including ones specific to Nx. To install them, run the following command: Then, we can create a default .eslintrc.json file in the root of our project: The above ESLint configuration will, by default, apply Nx's module boundary rules to any TypeScript or JavaScript files in your project. It also applies its recommended rules for JavaScript and TypeScript respectively, and gives you room to add your own. You can also have ESLint configurations specific to your apps and libraries. For example, if you have a React app, you can add a .eslintrc.json file to the root of your app directory with the following contents: Set Up Type Checking Type checking with tsc is normally a very straightforward process. You can just run tsc noEmit to check your code for type errors. But things are more complicated in Nx with lint staged. There are a two tricky things about type checking with lint staged in an Nx monorepo. First, different apps and libraries can have their own tsconfig.json files. When type checking each app or library, we need to make sure we're using that specific configuration. The second wrinkle comes from the fact that lint staged passes a list of staged files to commands it runs by default. And tsc will only accept either a specific tsconfig file, or a list of files to check. We do want to use the specific tsconfig.json files, and we also only want to run type checking against apps and libraries with changes. To do this, we're going to create some Nx run commands within our apps and libraries and run those instead of calling tsc directly. Within each app or library you want type checked, open the project.json file, and add a new run command like this one: Inside commands is our type checking command, using the local tsconfig.json file for that specific Nx app. The cwd option tells Nx where to run the command from. The forwardAllArgs option tells Nx to ignore any arguments passed to the command. This is important because tsc will fail if you pass both a tsconfig.json and a list of files from lint staged. Now if we ran nx affected target=typecheck from the command line, we would be able to type check all affected apps and libraries that have a typecheck target in their project.json. Next we'll have lint staged handle this for us. Installing Husky and lint staged Finally, we'll install and configure Husky and lint staged. These are the two packages that will allow us to run commands on staged files before a commit is made. In your package.json file, add the prepare script to run Husky's install command: Then, run your prepare script to set up git hooks in your repository. This will create a .husky directory in your project root with the necessary file system permissions. The next step is to create our pre commit hook. We can do this from the command line: It's important to use Husky's CLI to create our hooks, because it handles file system permissions for us. Creating files manually could cause problems when we actually want to use the git hooks. After running the command, we will now have a file at .husky/pre commit that looks like this: Now whenever we try to commit, Husky will run the lint staged command. We've given it some extra options. First, concurrent false to make sure attempts to write fixes with formatting and linting don't conflict with simultaneous attempts at type checking. Second is relative, because our Nx commands for formatting and linting expect a list of file paths relative to the repo root, but lint staged would otherwise pass the full path by default. We've got our pre commit command ready, but we haven't actually configured lint staged yet. Let's do that next. Configuring lint staged In a simpler repository, it would be easy to add some lint staged configuration to our package.json file. But because we're trying to check a complex monorepo in Nx, we need to add a separate configuration file. We'll call it lint staged.config.js and put it in the root of our project. Here is what our configuration file will look like: Within our module.exports object, we've defined two globs: one that will match any TypeScript files in our apps, libraries, and tools directories, and another that also matches JavaScript and JSON files in those directories. We only need to run type checking for the TypeScript files, which is why that one is broken out and narrowed down to only those files. These globs defining our directories can be passed a single command, or an array of commands. It's common with lint staged to just pass a string like tsc noEmit or eslint fix. But we're going to pass a function instead to combine the list of files provided by lint staged with the desired Nx commands. The nx affected and nx format:write commands both accept a files option. And remember that lint staged always passes in a list of staged files. That array of file paths becomes the argument to our functions, and we concatenate our list of files from lint staged into a comma delimitted string and interpolate that into the desired Nx command's files option. This will override Nx's normal behavior to explicitly tell it to only run the commands on the files that have changed and any other files affected by those changes. Testing It Out Now that we've got everything set up, let's try it out. Make a change to a TypeScript file in one of your apps or libraries. Then try to commit that change. You should see the following in your terminal as lint staged runs: Now, whenever you try to commit changes to files that match the globs defined in lint staged.config.js, the defined commands will run first, and verify that the files contain no type errors, linting errors, or formatting errors. If any of those commands fail, the commit will be aborted, and you'll have to fix the errors before you can commit. Conclusion We've now set up a monorepo with Nx and configured it to run type checking, linting, and formatting on staged files before a commit is made. This will help us catch errors before they make it into our codebase, and it will also help us keep our codebase consistent and readable. To see an example Nx monorepo with these configurations, check out this repo....

Tom VanAntwerp6 mins
Nx