Node.js, TypeScript, and Nest.js — A winning combination (and how chatGPT helped)
In this blog post, we will go over the advantages of using Node.js with TypeScript and Nest.js for building web services and also build a server using these technologies in only a few minutes. As a bonus, we’ll also discuss how chatGPT helped in the process.
To add a cherry on top, the webserver that we’ll build will have the following: logging (using Nest.js), documentation (using Swagger), metrics reporting (using prom-client), and two routes: an example route, and a route that can be used to fetch the metrics that were recorded.
Note that this server is very close to being production-ready: it’s still missing a database connection and a CI/CD pipeline but other than that it comes with almost everything we need to deploy a service!
In case you’re not familiar with Node.js/Typescript/Nest.js the following section will provide a light overview, but if you’re already familiar with the technologies mentioned above, feel free to skip it and go directly to the next “hands-on” section.
The Advantages of Node.js
Node.js is an excellent choice for quickly building web services due to its non-blocking I/O model, which allows it to handle many concurrent connections with high performance that is enabled by its V8 engine which was developed in Google.
Node.js can easily integrate with other technologies and is very easy to adopt, making it a popular choice for building microservices and APIs.
Node.js has a vibrant ecosystem of packages, or modules which can be used to add functionality to our application, such as connecting to a database or handling HTTP requests. These packages can be easily installed using the Node Package Manager (NPM) or Yarn.
In this article, we'll useyarn
as our package manager due to its growing popularity among the Node community. Yarn performs parallel installation which makes it much faster compared to NPM and is also considered more secure.
The Advantages of TypeScript
TypeScript is a statically-typed language that can help developers catch errors at compile-time instead of runtime. It provides a more robust type system than JavaScript, which can help improve code quality and maintainability. TypeScript also adds features like interfaces and types, which makes code more readable and easier to understand and debug.
TypeScript also provides better tooling support than JavaScript, with many popular editors like Visual Studio Code and WebStorm supporting TypeScript out of the box. This means that developers can take advantage of features like autocomplete, refactoring, and error highlighting while writing code.
The Advantages of Nest.js
Nest.js is an opinionated framework for building scalable and maintainable server-side applications on top of Node.js and Typescript.
Nest.js provides a modular architecture that allows developers to break their applications into smaller, reusable modules. This helps improve code organization, separation of concerns, and maintainability. Nest.js also provides many built-in features, such as support for Configuration, Testing (using Jest), Logging, and HTTP requests and responses, that can make it faster and easier to develop web services.
Nest.js also provides support for many popular Node.js packages, such as Mongoose for MongoDB integration and Passport for authentication which reduce the amount of code needed to build an application.
I hope that by now I was able to convince you about all the advantages of using Node.js with TypeScript and Nest.js and now we’re ready:
We’ll start with installing yarn, and make sure we’re running node v18.
If you’re not familiar with NVM (Node Version Manager) I recommend installing and using it to switch between different versions of Node.
We’ll start with installing Nest.js CLI globally:
npm i -g @nestjs/cli
Now we can create a new Nest.js project:
nest new <our-project-name>
This will run us through an installation of a new web server based on Express (the most popular Node.js web framework).
We’ll go to the project folder using: cd <our-project-name>
And install a few packages we need:
yarn add @nestjs/microservices @nestjs/swagger swagger-ui-express prom-client
Now open the project using your favorite IDE, most engineers like using either Visual Studio Code (free) or Webstorm (subscription based) — both are excellent options!
The file: package.json should look like this (if not, override yours with the following):
{
"name": "<your-project-name>",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^9.0.0",
"@nestjs/core": "^9.0.0",
"@nestjs/microservices": "^9.4.0",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/swagger": "^6.3.0",
"prom-client": "^14.2.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0",
"swagger-ui-express": "^4.6.2"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@types/express": "^4.17.13",
"@types/jest": "29.5.0",
"@types/node": "18.15.11",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "29.5.0",
"prettier": "^2.3.2",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "29.0.5",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.2.0",
"typescript": "^4.7.4"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
Let’s replace the code in main.js
with the following:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { MetricsInterceptor } from './metrics.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
// Apply the MetricsInterceptor globally
app.useGlobalInterceptors(new MetricsInterceptor());
const config = new DocumentBuilder()
.setTitle('Example API')
.setDescription('Example API description')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);
await app.listen(3000);
}
bootstrap();
The file src/app.service.ts:
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
// Method to return example data
getExampleData(name: string): string {
return `Hello${name ? ' ' + name : ''}, this is your example data!`;
}
}
The file src/app.module.ts:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MetricsController } from './metrics.controller';
@Module({
imports: [],
controllers: [AppController, MetricsController],
providers: [AppService],
})
export class AppModule {}
The file src/app.controller.ts:
import { Controller, Get, Query, Logger } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags, ApiQuery } from '@nestjs/swagger';
import { AppService } from './app.service';
@ApiTags('example')
@Controller('example')
export class AppController {
private readonly logger = new Logger(AppController.name);
constructor(private readonly appService: AppService) {}
@Get()
@ApiOperation({ summary: 'Get example data' })
@ApiResponse({ status: 200, description: 'Success' })
@ApiQuery({ name: 'name', required: false, description: 'Your name' })
getExampleData(@Query('name') name?: string): string {
// Log the query parameter (name) if provided
if (name) {
this.logger.log(`Name query parameter: ${name}`);
} else {
this.logger.log('No name query parameter provided');
}
return this.appService.getExampleData(name);
}
}
Pay special attention to the line that starts with @ApiQuery
— I added it so that you’ll have an example of how easy it is to add a query parameter to the route using Nest.js!
And of course, we need some tests, so we’ll update the test that Nest.js auto-generated for us.
The file src/app.controller.spec.ts:
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello, this is your example data!"', () => {
expect(appController.getExampleData()).toBe(
'Hello, this is your example data!',
);
});
});
});
Now let’s add src/metrics.interceptor.ts:
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Counter, Histogram, register } from 'prom-client';
@Injectable()
export class MetricsInterceptor implements NestInterceptor {
private requestCounter: Counter<string>;
private errorCounter: Counter<string>;
private requestLatency: Histogram<string>;
constructor() {
this.requestCounter = new Counter({
name: 'http_requests_total',
help: 'Number of HTTP requests',
labelNames: ['method', 'route', 'status'],
});
this.errorCounter = new Counter({
name: 'http_errors_total',
help: 'Number of HTTP errors',
labelNames: ['method', 'route', 'status'],
});
this.requestLatency = new Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request latency in seconds',
labelNames: ['method', 'route', 'status'],
buckets: [0.1, 0.5, 1, 5, 10],
});
}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const requestStartTime = Date.now();
const httpContext = context.switchToHttp();
const request = httpContext.getRequest();
return next.handle().pipe(
tap(
() => {
this.recordMetrics(request, requestStartTime, false);
},
() => {
this.recordMetrics(request, requestStartTime, true);
},
),
);
}
private recordMetrics(request, requestStartTime, isError: boolean) {
const latency = (Date.now() - requestStartTime) / 1000;
const route = request.route.path;
const method = request.method;
const status = request.res.statusCode;
this.requestCounter.labels(method, route, status).inc();
this.requestLatency.labels(method, route, status).observe(latency);
if (isError) {
this.errorCounter.labels(method, route, status).inc();
}
}
}
and a route to display the metrics: src/metrics.controller.ts:
import { Controller, Get, Res } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Response } from 'express';
import { register } from 'prom-client';
@Controller('metrics')
@ApiTags('metrics')
export class MetricsController {
@Get()
@ApiOperation({ summary: 'Get metrics' })
@ApiResponse({ status: 200, description: 'Success' })
async getMetrics(@Res() res: Response) {
res.set('Content-Type', register.contentType);
const metrics = await register.metrics();
res.end(metrics);
}
}
And we should be ready to go!
Before we begin, we want to make sure that all the packages in package.json are installed by running the following command from the root of the project:
yarn install
To start the server we can use yarn start
but I like using:
yarn run start:dev
because it reloads the application every time we make a code change so we can get quick feedback from the app.
Now we should be able to browse http://localhost:3000/docs and see a swagger page of our app which describes the existing routes and also allows us to “play” with them by submitting HTTP requests and see the responses just like using Postman.
We can now access: http://localhost:3000/api/example and see “Hello, this is your example data!” displayed, and we can also pass it a “name” param:
on top of that, we can access: http://localhost:3000/api/metrics and see our metrics reporting using Prometheus (an industry standard), which should look something like the following:
If we want to add a new controller/module/service/class/interface along with many other options, we can use nest generate ...
(CLI generate command) and Nest.js will add scaffolding code for us.
For example, I wanted to add a new route called openai
so I went back to command line and typed the following commands:
nest g module openai
nest g controller openai
nest g service openai
Now we have a server up and running with documentation (swagger), logging, and metric reporting as we promised.
All that’s left to do is add some “real” business logic to the service!
I urge you to go ahead and start playing with it — fun is guaranteed!
SideNote
When I wrote this blog post chatGPT helped me with multiple suggestions both about the code as well as finding some of the technical details I needed (I’m using it as a substitution to “Google”).
To get some motivation here’s a small example of a task I gave it a few days ago: I asked chatGPT to create a game of snake in Javascript including HTML/CSS and it created this. If you want to play it, just hit “play”, click with your mouse on the bottom-right pane, and start playing with the arrows. It took less than a minute…
I also used chatGPT to help me build a server that calls OpenAI APIs :)
It wasn’t perfect and needed some debugging and correcting but all in all, it still saved me valuable time and put me on the right track. One of the nice things about working with GPT4 is that when we run into an error — we can feed it the error and provide the line that caused it, and many times it will figure out what needs to be corrected by itself — so it’s a great debugging tool as well!
Bottom line, in case you haven’t started experimenting with chatGPT — I highly recommend it!
The code described in this article with an additional route that uses OpenAI API (to submit prompts and receive a response from chatGPT) can be found here: