Node.js Microservice with NestJS: Part 2 - Using the NestJS CLI

Introducing the Nest CLI

The NestJS framework provides a Command Line Interface (CLI) that can be used to generate NestJS projects and components of various types, such as services and controllers. It is build on top of the angular schematics (@schematics/angular) code generation framework.  In this second installment on building a NestJS microservice, we will use the Nest CLI to bootstrap our tour-service microservice into the autotoor-app repo.

For more details about the service we are building and what we have done so far, check out the following posts:

Installing the CLI

Since our demo project is using pnpm we will use it to install the Nest CLI as follows:

pnpm add -g @nestjs/cli

Instructions for installing the Nest CLI in other ways can be found here.

Creating the tour-service subproject

Since we are adding the tour service to our existing autotoor-app repo, we will want to put it into the apps directory.  Once there, we can use the Nest CLI to create the project as if it were a standalone Nest project.

We will use the following command to do so:
nest new tour-service --package-manager=pnpm --skip-git
Since we are not using the default package manager, we specify pnpm using the --package-manager option.  Additionally since we are creating this project within an existing repo, we specify the --skip-git option.  Otherwise the CLI will initialize a new git repo within the project, which would cause issues when we attempt to commit the new code to the autotoor-app repo.  If we had not used the --skip-git option when creating our project, we could have alternatively deleted the .git directory that it would have put in the tour-service directory afterwards.

Below is the full output of the command:
➜  autotoor-app git:(nestjs-service-2-cli) ✗ cd apps
➜  apps git:(nestjs-service-2-cli) ✗ nest new tour-service --package-manager=pnpm --skip-git
⚡  We will scaffold your app in a few seconds..

CREATE tour-service/.eslintrc.js (663 bytes)
CREATE tour-service/.prettierrc (51 bytes)
CREATE tour-service/README.md (3347 bytes)
CREATE tour-service/nest-cli.json (171 bytes)
CREATE tour-service/package.json (1951 bytes)
CREATE tour-service/tsconfig.build.json (97 bytes)
CREATE tour-service/tsconfig.json (546 bytes)
CREATE tour-service/src/app.controller.ts (274 bytes)
CREATE tour-service/src/app.module.ts (249 bytes)
CREATE tour-service/src/app.service.ts (142 bytes)
CREATE tour-service/src/main.ts (208 bytes)
CREATE tour-service/src/app.controller.spec.ts (617 bytes)
CREATE tour-service/test/jest-e2e.json (183 bytes)
CREATE tour-service/test/app.e2e-spec.ts (630 bytes)

✔ Installation in progress... ☕

🚀  Successfully created project tour-service
👉  Get started with the following commands:

$ cd tour-service
$ pnpm run start


                          Thanks for installing Nest 🙏
                 Please consider donating to our open collective
                        to help us maintain this package.


               🍷  Donate: https://opencollective.com/nest

Notice that it creates quite a few files.  This is because it creates a few sample components which we will delete in a minute and instead create our own.   That said, in its current state, the app is a functional NestJS application.  You can run it from within the tour-service directory as follows:
➜  apps git:(nestjs-service-2-cli) ✗ cd tour-service
➜  tour-service git:(nestjs-service-2-cli) ✗ pnpm run start

> tour-service@0.0.1 start /Users/alexlevine/dev/autotoor-app/apps/tour-service
> nest start

[Nest] 12688  - 06/30/2024, 12:14:58 PM     LOG [NestFactory] Starting Nest application...
[Nest] 12688  - 06/30/2024, 12:14:58 PM     LOG [InstanceLoader] AppModule dependencies initialized +14ms
[Nest] 12688  - 06/30/2024, 12:14:58 PM     LOG [RoutesResolver] AppController {/}: +18ms
[Nest] 12688  - 06/30/2024, 12:14:58 PM     LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 12688  - 06/30/2024, 12:14:58 PM     LOG [NestApplication] Nest application successfully started +1ms
Now if you go to http://localhost:3000 in your browser you should see the following:


What you are seeing in action is the sample app that the Nest CLI generates out of the box.  It creates all the standard configuration files such as: package.json, eslintrc.js, .prettierrc, nest-cli.json, tsconfig.json, tsconfig.build.json, as well as a test directory with some sample tests.  The main parts of the application that you see running are the following:

main.ts - This file bootstraps the NestJS application on port 3000 and loads its primary module called AppModule.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

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

app.module.ts - This file defines the main application module AppModule that is then responsible for loading all user defined modules, services, controllers, etc.  This includes the AppService and AppController
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

app.service.ts - This defines the sample AppService which has one method, getHello, which returns the string "Hello World" when called.
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

app.controller.ts - this file defines the AppController which defines a single route at / that then calls AppService.getHello to return a response, thus returning "Hello World" when an HTTP GET request is made to http://localhost:3000
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

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

@Get()
getHello(): string {
return this.appService.getHello();
}
}

This is great, but not what we want in our actual application.  All we really want out of this, other than the project files, are main.ts and app.module.ts, so lets delete a few things.

➜  tour-service git:(nestjs-service-2-cli) ✗ rm src/app.controller.spec.ts src/app.controller.ts src/app.service.ts
➜  tour-service git:(nestjs-service-2-cli) ✗ rm -rf test
We also need to remove refs in app.module.ts. Lets delete the imports for app.controller and app.service as well as the controllers and providers arrays.
import { Module } from '@nestjs/common';

@Module({
imports: [],
})
export class AppModule {}

Now we are ready to add our own module, service and controller.

Adding a Module with the Nest CLI

Rather than throwing everything in the main AppModule, NestJS best practices dictate that we should bundle groups of features into separate contained modules that we then import into the main AppModule.  In that vein, lets add a new module called LandmarkModule.  This will be the module that handles searching Landmarks and returning information about them to our FE via a REST API.

The following CLI command: nest g module landmark run from within the tour-service src directory will create the LandmarkModule and automatically add it to the AppModule:

➜  tour-service git:(nestjs-service-2-cli) ✗ cd src
➜  src git:(nestjs-service-2-cli) ✗ nest g module landmark
CREATE landmark/landmark.module.ts (85 bytes)
UPDATE app.module.ts (207 bytes)

This creates a new landmark module directory with a new empty LandmarkModule file in it:

landmark/landmark.module.ts
import { Module } from '@nestjs/common';

@Module({})
export class LandmarkModule {}
It also imports the LandmarkModule into AppModule so that it gets loaded by the NestJS application:

app.module.ts
import { Module } from '@nestjs/common';
import { LandmarkModule } from './landmark/landmark.module';

@Module({
imports: [LandmarkModule],
})
export class AppModule {}

Adding a Service with the Nest CLI

Now lets add a service called LandmarkService to the LandmarkModule.  We can do so by using the Nest CLI command: nest g service landmark --no-spec --flat from within the landmark directory.  A few things to note:
  1. I use the --no-spec option because the Nest CLI will generate a test for the LandmarkService called landmark.service.spec.ts in the same directory that uses the NestJS TestingModule (to be covered in a later post) and I prefer to write lighter unit tests and have them located in a separate test directory (we will also cover this in a later post).
  2. I use the --flat option because by default the Nest CLI will generate a directory for everything and I want it to be located in the landmark module directory which I am currently in.
➜  src git:(nestjs-service-2-cli) ✗ cd landmark
➜  landmark git:(nestjs-service-2-cli) ✗ nest g service landmark --no-spec --flat
CREATE landmark.service.ts (92 bytes)
UPDATE landmark.module.ts (172 bytes)
This generates an empty LandmarkService in the file landmark.service.ts

landmark/landmark.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class LandmarkService {}
and adds it to the LandmarkModule by updating landmark.module.ts

landmark/landmark.module.ts
import { Module } from '@nestjs/common';
import { LandmarkService } from './landmark.service';

@Module({
providers: [LandmarkService],
})
export class LandmarkModule {}
Note that the files it generates may not line up with your preferred coding style. For example, I used prettier to add the trailing comma after the providers array in the updated landmark.module.ts file.


Lets add some types that will be common to the entire project.  We will create a common directory right under src and add the following types.ts and index.ts files to it:

common/types.ts
export interface Coordinates {
latitude: number;
longitude: number;
}

export enum DistanceUnit {
FOOT = 'FOOT',
KILOMETER = 'KILOMETER',
METER = 'METER',
MILE = 'MILE',
}

export interface LocalLandmarkCriteria {
/**
* The coordinates to search from
*/
coordinates: Coordinates;

/**
* The maximum number of results
*/
maxCount: number;

/**
* The unit of distance to use for the search radius and results.
*/
distanceUnit: DistanceUnit;
}

export interface LandmarkDetails {
/**
* The id of the landmark
*/
id: string;

/**
* The title of the landmark.
*/
title: string;

/**
* URL where more info can be found
*/
url: string;

/**
* The url of the image to display for the landmark.
*/
imageUrl: string;

/**
* The summary of the landmark to read.
*/
readableSummary: string;

/**
* The location of the landmark.
*/
coordinates: Coordinates;

/**
* The provider of landmark data
*/
provider: string;
}

export interface LocalLandmarkDetails {
landmark: LandmarkDetails;

/**
* The distance of the landmark from the location.
*/
distance: number;

/**
* The distance unit of the distance.
*/
distanceUnit: DistanceUnit;
}

common/index.ts
export * from './types';

And now, let's add a method to the LandmarkService called getLocalLandmarks that returns a canned response as follows:

landmark/landmark.service.ts
import { Injectable } from '@nestjs/common';
import {
DistanceUnit,
LocalLandmarkCriteria,
LocalLandmarkDetails,
} from '../common';

@Injectable()
export class LandmarkService {
public async getLocalLandmarks(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
criteria: LocalLandmarkCriteria,
): Promise<LocalLandmarkDetails[]> {
console.log(
'Request made to LandmarkService.getLocalLandmarks with criteria',
criteria,
);
const landmarks: LocalLandmarkDetails[] = [
{
landmark: {
id: 'lm_1',
title: 'The First Landmark',
url: 'https://en.wikipedia.org/wiki/Aaron_A._Sargent_House',
imageUrl: 'http://img1.url',
coordinates: {
latitude: 39.2663886,
longitude: -121.02655320000001,
},
provider: 'provider-1',
readableSummary: 'The First Landmark Summary',
},
distance: 55,
distanceUnit: DistanceUnit.FOOT,
},
];
console.log(
'LandmarkService.getLocalLandmarks returning landmarks',
landmarks,
);
return landmarks;
}
}

Adding a Controller using the Nest CLI

Now that we have a service, we need to expose it via a REST API.  This is what our controller will do.  Lets add one using the Nest CLI.  We will use nearly the same command as that used to create the LandmarkService except using the controller schematic name instead of service. E.g nest g controller landmark --no-spec --flat
➜  landmark git:(nestjs-service-2-cli) ✗ nest g controller landmark --no-spec --flat
CREATE landmark.controller.ts (105 bytes)
UPDATE landmark.module.ts (269 bytes)
This creates the following empty landmark.controller.ts file:

landmark/landmark.controller.ts
import { Controller } from '@nestjs/common';

@Controller('landmark')
export class LandmarkController {}

It also imports the LandmarkController into the LandmarkModule so that it gets loaded by the NestJS application.

landmark/landmark.module.ts
import { Module } from '@nestjs/common';
import { LandmarkService } from './landmark.service';
import { LandmarkController } from './landmark.controller';

@Module({
providers: [LandmarkService],
controllers: [LandmarkController],
})
export class LandmarkModule {}

Now, let's add some code to our controller to expose the LandmarkService.getLandmarks method via a REST endpoint.
import { Controller, Get, Query } from '@nestjs/common';
import {
DistanceUnit,
LocalLandmarkCriteria,
LocalLandmarkDetails,
} from '../common';
import { LandmarkService } from './landmark.service';

@Controller('landmark/v1')
export class LandmarkController {
private readonly landmarkService: LandmarkService;

constructor(landmarkService: LandmarkService) {
this.landmarkService = landmarkService;
}

@Get('/landmark/local')
public async getLocalLandmarks(
@Query('latitude') latitude: number,
@Query('longitude') longitude: number,
@Query('distanceUnit') distanceUnit: DistanceUnit,
@Query('maxCount') maxCount: number,
): Promise<LocalLandmarkDetails[]> {
const criteria: LocalLandmarkCriteria = {
coordinates: {
latitude,
longitude,
},
maxCount,
distanceUnit,
};
console.log(
'Request made to LandmarkController.getLocalLandmarks with criteria',
criteria,
);
return this.landmarkService.getLocalLandmarks(criteria);
}
}
This controller will handle GET requests to the endpoint http://localhost:3000/landmark/local with query parameters latitude, longitude, maxCount, and distanceUnit.  We will do a deep dive into NestJS controllers and their decorators (@Controller, @Get, @Query) in a later blog post.

Let's start up our service and give it a try.  Again we can start up our service by running pnpm run start from the tour-service directory.
➜  tour-service git:(nestjs-service-2-cli) ✗ pnpm run start

> tour-service@0.0.1 start /Users/alexlevine/dev/autotoor-app/apps/tour-service
> nest start

[Nest] 15217  - 06/30/2024, 4:07:02 PM     LOG [NestFactory] Starting Nest application...
[Nest] 15217  - 06/30/2024, 4:07:02 PM     LOG [InstanceLoader] AppModule dependencies initialized +8ms
[Nest] 15217  - 06/30/2024, 4:07:02 PM     LOG [InstanceLoader] LandmarkModule dependencies initialized +0ms
[Nest] 15217  - 06/30/2024, 4:07:02 PM     LOG [RoutesResolver] LandmarkController {/landmark/v1}: +8ms
[Nest] 15217  - 06/30/2024, 4:07:02 PM     LOG [RouterExplorer] Mapped {/landmark/v1/landmark/local, GET} route +2ms
[Nest] 15217  - 06/30/2024, 4:07:02 PM     LOG [NestApplication] Nest application successfully started +2ms

You can see that the app starts up and it maps the LandmarkController routes.
Now if you visit the following url in a browser you should see the results below: 





A look in the logs shows the following output where you should be able to see all of the console log statements we added:
[Nest] 15344  - 06/30/2024, 4:15:56 PM     LOG [NestApplication] Nest application successfully started +1ms
Request made to LandmarkController.getLocalLandmarks with criteria {
  coordinates: { latitude: '1234', longitude: '5678' },
  maxCount: '10',
  distanceUnit: 'FOOT'
}
Request made to LandmarkService.getLocalLandmarks with criteria {
  coordinates: { latitude: '1234', longitude: '5678' },
  maxCount: '10',
  distanceUnit: 'FOOT'
}
LandmarkService.getLocalLandmarks returning landmarks [
  {
    landmark: {
      id: 'lm_1',
      title: 'The First Landmark',
      url: 'https://en.wikipedia.org/wiki/Aaron_A._Sargent_House',
      imageUrl: 'http://img1.url',
      coordinates: [Object],
      provider: 'provider-1',
      readableSummary: 'The First Landmark Summary'
    },
    distance: 55,
    distanceUnit: 'FOOT'
  }
]

Success! We have created a "working" app using the Nest CLI.  


Get the Code

All of the code from this post can be found in the apps/tour-service directory of the nestjs-service-2-cli git branch here: https://github.com/autotoor/autotoor-app/tree/nestjs-service-2-cli/apps/tour-service

Next Steps

I hope you have found these steps for scaffolding a basic NestJS application using the Nest CLI helpful.  In our next few posts we will dig deeper into each part of the app until we have a more production-grade application.  Some of the topics coming up include: Dependency Injection, Controllers, Logging, Testing, Sharing types in our monorepo and more...





Comments

Popular posts from this blog

Node.js Microservice with NestJS: Part 3 - NestJS Dependency Injection

REST APIs with Controllers - Node.js Microservice with NestJS: Part 4

Constructor-Based vs Property-Based Dependency Injection in NestJS - Which is Better?