DEV Community

Cover image for How to add a FREE MongoDB database to your NestJS API with TypeORM
Yohan Lasorsa for Microsoft Azure

Posted on

How to add a FREE MongoDB database to your NestJS API with TypeORM

If you are building an API for your application, one of the first questions you will probably ask yourself is where to store my data?

Most of the time the answer will be in a database, but which one? Especially if you are looking for good performance at a cheap price (or for free), your options are quite reduced. Good news, a free tier has been introduced for Azure Cosmos DB that is suitable for production workloads, with up to 5 GB storage included.

In this article, we will go through all the steps to configure and use an Azure Cosmos DB database in a new NestJS application.

TL;DR key takeaways

  • NestJS provides an excellent TypeORM integration that simplifies database usage a lot.
  • Azure Cosmos DB can be used with many different drivers including MongoDB, allowing easy integration with existing libraries.
  • While TypeORM primary focus is SQL databases, it also works very well with NoSQL using MongoDB.

Here is the final project source code on GitHub.

What will you learn here?

In this article, we will:

  • Bootstrap a Node.js API from scratch using NestJS
  • Create a free Azure Cosmos DB database with MongoDB support
  • Configure TypeORM for MongoDB connection
  • Create an entity to map your model with the database
  • Add CRUD endpoints to your NestJS app

Reference links for everything we use

Requirements

As an alternative, if you don't want to create an Azure subscription you can also use the Try Cosmos DB website to get access to a Cosmos DB trial instance.

Getting started

Your API will be built using NestJS.

If you're not familiar with NestJS, it's a TypeScript Node.js framework that looks a lot like Angular and helps you build enterprise-grade efficient and scalable Node.js applications.

Install NestJS CLI and bootstrap new server app

Use the following commands to install the NestJS CLI and create a new app:

$ npm install -g @nestjs/cli
$ nest new pets-api
$ cd pets-api
Enter fullscreen mode Exit fullscreen mode

We will create a simple pet management API as an example, so let's create a controller for that using this command:

$ nest generate controller pets
Enter fullscreen mode Exit fullscreen mode

You are now ready to integrate the database.

Configure Cosmos DB

Cosmos DB is a managed distributed NoSQL database that will allow you to save and retrieve data. It supports multiple data models and many well-known database APIs, including MongoDB that we will use for our application.

CosmosDB multi-model and different APIs illustration

First, we have to create a Cosmos DB account, which can hold one or more databases. Make sure you have an Azure account before going through these steps:

  1. Click on this link: Create Azure Cosmos DB Account. Log in if needed, then fill-up the form like this:

    mongoDB database creation option

    When you are finished, click on Review + create, then finally Create.

  2. Provisioning the database will take a few minutes, so you can continue to the next section and come back once it's finished. When it's ready, click on Go to resource.

  3. Click on the Data Explorer tab, then on the New Collection button:

    screenshot of data explorer

  4. Fill in the fields like this:

    screenshot of new collection creation

    There are two things worth mentioning here:

    • We choose to share a provisioned throughput of Request Units among all our collections within our database, using the checkbox Provision database throughput. This greatly helps to reduce costs when using a paid account.
    • We need to define a shard key (also called partition key) for the collection, to ensure proper partitioning. We use the default auto-generated _id property by MongoDB for that.
  5. Finally, go to the Connection strings tab and click on the button next to your primary connection string to copy it:

    Screenshot of connection strings

Now create a .env file in your project root with these values:

MONGODB_CONNECTION_STRING=<your primary connection string>
MONGODB_DATABASE=pets-db
Enter fullscreen mode Exit fullscreen mode

Note: you should NEVER commit the .env file to your repository! This file is only for local testing, so add it to your .gitignore file.

These values will be exposed to your app as environment variables during development to access to your database. To do that we use the @nestjs/config package that provides dotenv integration:

npm i @nestjs/config
Enter fullscreen mode Exit fullscreen mode

Open the file src/app.module.ts and add this to the module imports:

...
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot(),
    ...
  ]
Enter fullscreen mode Exit fullscreen mode

That's all we need for now, but keep in mind that @nestjs/config provides a lot more options for advanced needs.

Integrate with NestJS

You are now ready to use the database in your application. NestJS provides a great integration with TypeORM which is the most mature Object Relational Mapper (ORM) available for TypeScript, so we will use that.

First, you have to install a few more packages with this command:

npm install @nestjs/typeorm typeorm mongodb
Enter fullscreen mode Exit fullscreen mode

Open the file src/app.module.ts and add TypeOrmModule to the module imports:

import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mongodb',
      url: process.env.MONGODB_CONNECTION_STRING,
      database: process.env.MONGODB_DATABASE,
      entities: [
        __dirname + '/**/*.entity{.ts,.js}',
      ],
      ssl: true,
      useUnifiedTopology: true,
      useNewUrlParser: true
    }),
    ...
  ]
Enter fullscreen mode Exit fullscreen mode

Tip: Using process.env.<VARIABLE_NAME> in place of hardcoded values allows to keep sensitive information out of your codebase and read them from environment variables instead. This also allows you to deploy the exact same code on different environments (like staging and production for example), but with different configurations, as recommended in the 12-factor app best practices.

TypeORM will discover and map your entities following the *.entity.ts (.js once compiled) naming scheme, as specified in the module options.

But hey, we don't have an entity yet? That's right, let's create it!

Create an entity

A database entity is used to model the properties of whatever object you would like to store. In our case, we would like to store pet data, so let's create define a Pet entity.

Create a new file src/pets/pet.entity.ts with this code:

import { Entity, ObjectID, ObjectIdColumn, Column } from 'typeorm';

@Entity('pets')
export class Pet {
  @ObjectIdColumn() id: ObjectID;
  @Column() name: string;
  @Column() animalType: string;
  @Column() pictureUrl?: string;
  @Column() birthDate?: Date;

  constructor(pet?: Partial<Pet>) {
    Object.assign(this, pet);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now let's break down the annotations we have used:

  • @Entity marks the class as a TypeORM entity to be stored into the pets collection.
  • @ObjectIdColumn marks the unique identifier of an entity that will be mapped to the mandatory MongoDB _id property. It will be automatically generated if you don't provide one.
  • @Column marks the properties you want to map to a table column. The type of property will also define the type of data that will be stored.

Note: for more complex domain models you can define subdocuments using simple type references, see this example.

Inject the repository

TypeORM supports the repository design pattern, and @nestjs/typeorm package provides you an easy way to declare injectable repositories for each of your entities.

Open the file src/app.module.ts again and add this to the module imports:

import { Pet } from './pets/pet.entity';

@Module({
  imports: [
    TypeOrmModule.forFeature([Pet]),
    ...
  ]
Enter fullscreen mode Exit fullscreen mode

Now you can inject your Pet repository using the annotation @InjectRepository. Open the file src/pets/pets.controller.ts and add this constructor:

@Controller('pets')
export class PetsController {
  constructor(
    @InjectRepository(Pet)
    private readonly petsRepository: MongoRepository<Pet>,
  ) {}
  ...
}
Enter fullscreen mode Exit fullscreen mode

Don't forget to add these missing imports at the top of the file:

import { InjectRepository } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm';
import { ObjectID } from 'mongodb';
import { Pet } from './pet.entity';
Enter fullscreen mode Exit fullscreen mode

You can now use this.petsRepository within your controller to perform CRUD operations (method signatures were simplified for readability):

  • save(entity: PartialEntity<Entity> | PartialEntity<Entity>[], options?: SaveOptions): Promise<Entity | Entity[]>: inserts one or more entities in the database if they do not exists, updates otherwise.
  • findOne(criteria?: ObjectID | FindOneOptions<Entity>): Promise<Entity | undefined>: finds the first entity matching an ID or query options.
  • find(criteria?: FindManyOptions<Entity>): Promise<Entity[]>: finds all entities that match the specified criteria (return all entities if none is provided).
  • update(criteria: ObjectID | ObjectID[] | FindConditions<Entity>, partialEntity: PartialEntity<Entity> | PartialEntity<Entity>[]): Promise<UpdateResult>: updates entities matching the specified criteria. It allows partial updates, but does not check if entities exists.
  • delete(criteria: ObjectID | ObjectID[] | FindConditions<Entity>): Promise<DeleteResult>: removes one or more entities matching the specified criteria from the database. Does not check if entities exists.

In all these methods, you can either use the entity ID or a regular MongoDB query to match specific entities. For example, you can use:

// Find all cats
await this.petsRepository.find({ animalType: 'cat' });

// Find the pet with the specified ID
await this.petsRepository.findOne(id);
Enter fullscreen mode Exit fullscreen mode

Add new endpoints

Now you have everything needed to create your CRUD (Create, Read, Update and Delete) endpoints:

GET /pets         // Get all pets
GET /pets/:id     // Get the pet with the specified ID
POST /pets        // Create a new pet
PUT /pets/:id     // Update the pet with the specified ID
DELETE /pets/:id  // Delete the pet with the specified ID
Enter fullscreen mode Exit fullscreen mode

Read

Let's start with the first one, to get all the pets. Add this method to your controller:

@Get()
async getPets(): Promise<Pet[]> {
  return await this.petsRepository.find();
}
Enter fullscreen mode Exit fullscreen mode

Easy right 😎? By not specifying any criteria for the find() method, all entities in the collection will be returned.

Now continue with the next one, to retrieve a single pet using its ID:

@Get(':id')
async getPet(@Param('id') id): Promise<Pet> {
  const pet = ObjectID.isValid(id) && await this.petsRepository.findOne(id);
  if (!pet) {
    // Entity not found
    throw new NotFoundException();
  }
  return pet;
}
Enter fullscreen mode Exit fullscreen mode

We use the @Get() annotation like previously, but this time we add a route parameter using :id. This parameter can then be retrieved with the function arguments using the @Param('id') annotation.

We check that the provided string is a valid MongoDB ObjectID and then we call the petsRepository.findOne() method to find the matching entity. In case it's not found or if the provided ID is invalid, we return a status 404 error using NestJS predefined exception class NotFoundException.

Create

Now for the pet creation:

@Post()
async createPet(@Body() pet: Partial<Pet>): Promise<Pet> {
  if (!pet || !pet.name || !pet.animalType) {
    throw new BadRequestException(`A pet must have at least name and animalType defined`);
  }
  return await this.petsRepository.save(new Pet(pet));
}
Enter fullscreen mode Exit fullscreen mode

Here we use the @Body() annotation as a function parameter to retrieve the request data for our pet. We also add basic validation and return a status 400 error with a message, using NestJS BadRequestException.

Tip: for more advanced validation techniques using DTOs (Data Transfer Objects) and annotations you can take a look at this documentation.

Update

For the update endpoint, it's sort of a mix between read and create:

@Put(':id')
@HttpCode(204)
async updatePet(@Param('id') id, @Body() pet: Partial<Pet>): Promise<void> {
  // Check if entity exists
  const exists = ObjectID.isValid(id) && await this.petsRepository.findOne(id);
  if (!exists) {
    throw new NotFoundException();
  }
  await this.petsRepository.update(id, pet);
}
Enter fullscreen mode Exit fullscreen mode

We added the annotation @HttpCode(204) to change the HTTP status to 204 (No Content) as we don't return anything if the update succeeds. We also need to check if an entity exists before updating it.

Delete

Finally, we add the delete method which looks a lot like the previous one:

@Delete(':id')
@HttpCode(204)
async deletePet(@Param('id') id): Promise<void> {
  // Check if entity exists
  const exists = ObjectID.isValid(id) && await this.petsRepository.findOne(id);
  if (!exists) {
    throw new NotFoundException();
  }
  await this.petsRepository.delete(id);
}
Enter fullscreen mode Exit fullscreen mode

CRUD endpoints, done βœ”οΈ.

If you want to know more about available annotations and helpers you can use in your controllers, you can look at the NestJS documentation and the TypeORM documentation.

Test your endpoints

Now it's time to test if your REST API works, start your server with the command:

npm run start
Enter fullscreen mode Exit fullscreen mode

When the server is started, you can test if your new endpoints behave correctly using curl:

curl http://localhost:3000/pets
# should return an empty list: []

curl http://localhost:3000/pets/0
# should return 404 with an error

curl http://localhost:3000/pets \
  -X POST \
  -H "content-type: application/json" \
  -d '{ "name": "Garfield", "animalType": "cat" }'
# should return the newly created pet

curl http://localhost:3000/pets
# should return a list including the previously added pet

curl http://localhost:3000/pets/<id_from_post_command> \
  -X PUT \
  -H "content-type: application/json" \
  -d '{ "pictureUrl": "https://placekitten.com/400/600" }'
# should update the pet

curl http://localhost:3000/pets/<id_from_post_command>
# should return the updated pet

curl http://localhost:3000/pets/<id_from_post_command> \
  -X DELETE
# should delete the pet
Enter fullscreen mode Exit fullscreen mode

Explore your data

Once you have played a bit with your API and created some pets, why not take a look at your data?

You can either use the standalone Storage Explorer application for that or go to the Azure portal and access the online version.

We only want to give a quick look, so let's use the online version:

  1. Go back to portal.azure.com

  2. Use the search bar at the top and enter the name of the Cosmos DB account you created, then click on it in the search results:

    searching your Cosmos DB account

  3. Click on Storage Explorer in the resource menu, then unfold the pets-db database and pets collection to open the Documents where your data lives in:

    storage explorer

From there, you can query your pets, edit or delete them and even create new ones.
This tool can be helpful to quickly check your data visually and debug things when something's wrong.

Going further

This was a brief introduction, but you have already seen how quickly you can create a basic CRUD API with NestJS and Azure Cosmos DB.

If you want to dig further into NestJS or Azure, here are some resources I recommend:


Follow me on Twitter, I would be happy to discuss and take your suggestions!

Top comments (2)

Collapse
 
selfrefactor profile image
Dejan Toteff

Much obliged Yohan.

I use your steps with Mongo Atlas and it worked like a charm.

Probably, I would never go through the hassle of setting of Nest.js with TypeORM and Monsoose, if I didn't have a roadmap in front of me thanks to you.

Collapse
 
sinedied profile image
Yohan Lasorsa

I'm glad it was helpful! And it's nice to hear it works with others hosted DB as well πŸ˜‰