"How to Set Up Data Efficient NestJS APIs with MikroORM" is a basic guide that details the process.

"How to Set Up Data Efficient NestJS APIs with MikroORM" is a basic guide that details the process.

Overview

I recently came across NestJS, a Node.js framework, and quickly became a fan after using it for a few months. I wanted to challenge myself and try something new, so I decided to use a node framework to create an API for a work POC instead of my usual .NET environment. After experimenting with HapiJS and Express, I chose NestJS because of its familiarity with Angular, my primary front-end framework.

I was able to set up a functional API within a day, and during this time, I discovered MikroORM, an impressive NodeJS ORM. I tested TypeORM, Sequelize, and Prisma, but MikroORM was the best fit for me as it served as a NodeJS replacement for an entity framework core like ORM. I prefer not to meddle with the database unless it's for optimization purposes. 😅

MikroORM features

  1. TypeScript Support: Built on TypeScript and giving developers a rock-solid typing system for bug-less applications.

  2. Entity Management: It provides an entity-based approach to managing database models, making it easier to define, query, and manipulate database records using familiar object-oriented concepts.

  3. Data Migrations: Facilitates the management of database schema changes through its migration system. This ensures that the database structure remains synchronized with the application's evolving data model.

  4. Multiple Database Support: It supports various database systems like PostgreSQL, MySQL, SQLite, and MongoDB, allowing developers to choose the most suitable option for their projects.

  5. Query Builder: Includes a powerful query builder that enables developers to construct database queries using a fluent and chain-able syntax. This abstraction simplifies complex querying tasks.

  6. Caching: The library supports caching mechanisms to optimize database performance by reducing the need for repeated queries, and enhancing overall application speed.

  7. Transaction Management: Handles database transactions effectively, ensuring data consistency and integrity within the application's operations.

  8. Lazy Loading: It provides lazy loading of related entities, optimizing database queries by loading associated data only when necessary.

  9. Hooks and Listeners: Allows developers to define hooks and listeners, enabling them to react to specific events in the database lifecycle, such as before or after persisting entities.

  10. Integration with NestJS: Integrates seamlessly with NestJS, leveraging its module-based architecture and decorators to simplify the setup and usage of the ORM within a NestJS application.

In this blog, we'll focus on Entity Management and Integration with NestJS

Prerequisites

  • NodeJS installed, preferably version 16+

  • Your choice of editor e.g. VSCode or WebStorm

Create a NestJS application

For the simplicity of this demo, we'll create a simple task API. We'll name it TaskyAPI😎.

Let's start by installing the NestJS CLI so we can create our application, we'll then add the ORM packages we need.

type the following command in your terminal to create the API application

npm i -g @nestjs/cli # install the global nestjs cli
nest new tasky-api

After selecting your choice of package manager in the prompt, you should see an output similar to this.

⚡  We will scaffold your app in a few seconds..

? Which package manager would you ❤️  to use? npm
CREATE tasky-api/.eslintrc.js (663 bytes)
CREATE tasky-api/.prettierrc (51 bytes)
CREATE tasky-api/README.md (3340 bytes)
CREATE tasky-api/nest-cli.json (171 bytes)
CREATE tasky-api/package.json (1954 bytes)
CREATE tasky-api/tsconfig.build.json (97 bytes)
CREATE tasky-api/tsconfig.json (546 bytes)
CREATE tasky-api/src/app.controller.spec.ts (617 bytes)
CREATE tasky-api/src/app.controller.ts (274 bytes)
CREATE tasky-api/src/app.module.ts (249 bytes)
CREATE tasky-api/src/app.service.ts (142 bytes)
CREATE tasky-api/src/main.ts (208 bytes)
CREATE tasky-api/test/app.e2e-spec.ts (630 bytes)
CREATE tasky-api/test/jest-e2e.json (183 bytes)

✔ Installation in progress... ☕

🚀  Successfully created project tasky-api

Install packages.

We install the uuid package to generate our primary key, you can easily use the built in identity feature of MikroORM. I just prefer using uuid for the ID column in my tables.

We'll also use SQLite to simplify this demo, MikroORM does support other databases, see the list here.

Let's change the directory into the application folder and then install our packages.

cd tasky-api
npm i -s @mikro-orm/core @mikro-orm/sqlite @mikro-orm/nestjs @mikro-orm/sql-highlighter uuid

Then we open our project in your choice of editor then update the tsconfig.json file with the following, this will enable support for decorators in TypeScript.

"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,

Our task model

Since we're using an ORM, we will model out data from an entity type then we will use that type, generate database migrations tables, and also send CRUD commands to our database.

Create the entities folder in the project and add a new task.entity.ts file with our TaskEntity

import { BaseEntity, Entity, PrimaryKey, Property } from '@mikro-orm/core';
import { v4 } from 'uuid';

@Entity({
  tableName: 'tasks',
})
export class TaskEntity extends BaseEntity<TaskEntity, 'id'>{
  @PrimaryKey({ type: 'uuid' })
  id: string = v4();

  @Property({ type: 'text', length: 500 })
  description!: string;

  @Property({ type: 'boolean', default: false })
  completed!: boolean;
}

Your project should look like this afterward.

CleanShot 2023-08-03 at 23.28.53.png

With that, we're ready to wire up MikroORM with NestJS.

Configure MikroORM to work with NestJS

Open the app.module.ts file and add the following MikroOrmModule configurations. This is a basic setup that uses SQLite as a database with one entity.

We're also using the ensureDatabase option which will create the database and tables for us based on on TaskEntity.

NB! In a production environment this is not recommended, please see https://docs.nestjs.com/recipes/mikroorm for more advanced setups.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { TaskEntity } from './entities/task.entity';

@Module({
  imports: [
    MikroOrmModule.forRoot({
      ensureDatabase: true, // for testing purposes only, do not use it in production!
      autoLoadEntities: true, // sets auto discovery of entities
      dbName: 'tasky.sqlite3',
      type: 'sqlite',
      highlighter: new SqlHighlighter(),
    }),
    MikroOrmModule.forFeature([TaskEntity]), // registers the entity we created
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Let's create a simple CRUD service, open the app.service.ts and add the following code.

import { Injectable, NotFoundException } from '@nestjs/common';
import { EntityManager, QueryOrder } from '@mikro-orm/core';
import { TaskEntity } from './entities/task.entity';

@Injectable()
export class AppService {
  constructor(private entityManager: EntityManager) {}

  findAll(completed?: boolean) {
    return this.entityManager.find(
      TaskEntity,
      {
        ...(completed !== undefined && {
          completed,
        }),
      },
      {
        orderBy: {
          createdAt: QueryOrder.ASC,
        },
      },
    );
  }

  findOne(id: string) {
    return this.entityManager.findOneOrFail(
      TaskEntity,
      { id },
      {
        failHandler: () => {
          throw new NotFoundException('Task not found');
        },
      },
    );
  }

  async create(description: string) {
    const task = new TaskEntity();
    task.description = description;
    await this.entityManager.persistAndFlush(task);
    return task;
  }

  async update(id: string, description: string, completed: boolean) {
    const task = await this.entityManager.findOneOrFail(
      TaskEntity,
      { id },
      {
        failHandler: () => {
          throw new NotFoundException('Task not found');
        },
      },
    );

    task.description = description;
    task.completed = completed;
    await this.entityManager.persistAndFlush(task);
    return task;
  }

  async delete(id: string) {
    const task = await this.entityManager.findOneOrFail(
      TaskEntity,
      { id },
      {
        failHandler: () => {
          throw new NotFoundException('Task not found');
        },
      },
    );

    await this.entityManager.removeAndFlush(task);
  }
}

Let's expose the CRUD service as an API, open the app.controller.ts file and paste the following.

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Post,
  Query,
} from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  findTasks(@Query('completed') completed?: boolean) {
    return this.appService.findAll(completed);
  }

  @Get(':id')
  findOneTask(@Param('id') id: string) {
    return this.appService.findOne(id);
  }

  @Post()
  createTask(@Body() createTaskDto: { description: string }) {
    return this.appService.create(createTaskDto.description);
  }

  @Post(':id')
  updateTask(
    @Param('id') id: string,
    @Body() updateTaskDto: { description: string; completed: boolean },
  ) {
    return this.appService.update(
      id,
      updateTaskDto.description,
      updateTaskDto.completed,
    );
  }

  @Delete(':id')
  deleteTask(@Param('id') id: string) {
    return this.appService.delete(id);
  }
}

Finally, we'll configure MikroORM to create our database schema when we run the application.

💡
NB! This is a basic setup and this should not be used in production, loss of data is inevitable.

Open the main.ts file and add the following changes

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MikroORM } from '@mikro-orm/core';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const orm = app.get(MikroORM); // get mikro-orm
  await orm.getSchemaGenerator().refreshDatabase(); // create the database and tables

  await app.listen(3000);

  console.log(`Application is running on: http://localhost:3000`);
}
bootstrap();

Testing our API

With those steps completed, you should now have a working NestJS API that connects to a SQLite database with MikroORM.

Let's run our application and add a few tasks, run the app with the following command.

npm run start

You should see an output similar to this.

CleanShot 2023-08-04 at 00.22.57.png

Whoooooo hooooooo we have a working API!! 🔥 🔥 🔥

Let's use Postman and create a new task.

CleanShot 2023-08-04 at 00.29.12.png

Create a few more tasks using the same endpoint, now let's get our tasks using the get the tasks API.

CleanShot 2023-08-04 at 00.35.03.png

That's it folks, a simple API with simple database access that plugins in nicely into the NestJS DI system 🦾

Conclusion

In summary, my venture into NestJS and MikroORM has been transformative. Choosing NestJS for its familiarity and efficiency in API development led me to discover MikroORM, a potent TypeScript-based ORM that simplifies database interactions in Node.js apps, MikroORM's benefits are worth exploring.

I kept this article short on purpose so we can just touch the basics, stay tuned for a more advanced setup where we model complex database relationships, using migrations, transactions, and seeders soon.

MikroORM itself is a robust TypeScript-based ORM library, simplifying Node.js database interactions. Coupled with NestJS, it paves the way for efficient, scalable, and maintainable API development.

You can read more about NestJS at https://docs.nestjs.com and MikroORM at https://mikro-orm.io

This is my first-ever blog post and I hope someone finds it useful, thank you for reading and your feedback is highly valued.