Data exchange and integration with external systems is an integral part of any complex application. To make it possible, we should define a set of communication rules (contracts) and actions (methods) in a systematic way. REST API (REpresentational State Transfer Application Programming Interface) is a software architectural style which describes and uniforms an interface between two computer systems and allows information exchange securely over the internet.
REST API can be implemented using various frameworks and languages – in this blog, we will implement it using the NestJS framework. We are going to learn more about the framework and what makes it stand out across other Node.js frameworks, as well as a step-by-step guide, on how to implement a simple REST API.
About NestJS
NestJS is an open-source framework for building efficient and scalable server-side solutions. Created in 2017, it quickly gained popularity and is now one of the most popular choices to develop Node.js applications. Written in TypeScript, it combines elements of Object Oriented Programming, Functional Programming and Functional Reactive Programming. Compared to established and mature Node.js frameworks, such as Express, Koa or Fastify, it brings a more structured and well-defined approach to building back-end applications. Built on top of mentioned frameworks (Epress/Fastify), it provides an additional level of abstraction to streamline application development.
What truly makes NestJS stand out is the fact, that it is an opinionated framework. This means that the framework imposes certain rules, naming conventions or patterns to help develop scalable and maintainable solutions. It guides how to structure applications and divide complex functionality into smaller pieces. The goal is to build applications more consistently, in the same style, using the same concepts. This approach allows an application to scale and greatly simplifies application development.
Getting Started
Prerequisites: Make sure that Node.js (version >= 12,) is installed on your operating system.
To get started, we can use the Nest CLI tool to generate a starter project. The tool will create a new project directory, conventionally scaffold core Nest files and reference all required modules:
# Install Nest CLI tool $ npm i -g @nestjs/cli # Scaffold new project $ nest new nestjs-sample-app
Once the project is scaffolded, we should see the following directory content:
src - app.controller.spec.ts - app.controller.ts - app.module.ts - app.service.ts - main.ts
At first glance, we can see some structural similarities to popular Single Page Application (SPA) frameworks, particularly Angular. That is because the framework is heavily inspired by it, not only in terms of a directory structure but also strong dependency injection. The authors emphasize, the concept of Providers being fundamental to the framework and helping build a decoupled and scalable system, of which dependency injection is a crucial part.
We can also observe a certain pattern here, where the introduction of Controller and Service isolates the API and Business layers. With this fundamental assumption in mind, NestJS offers several common architectures to implement our system. An out-of-box structure can be expanded to support full Model-View-Controller (MVC), Command Query Responsibility Segregation (CQRS) or even Microservices architecture.
In each case, additional packages are provided, and documentation includes best practices and implementation guidelines. For the sake of simplicity, this guide will only focus on the default implementation, which consists of Service and Controller.
To run a newly created project; we should use the following command:
npm run start
Once the application is running, open your browser and navigate to http://localhost:3000/. You should be able to see the Hello World! message.
Note: Visit https://docs.nestjs.com/first-steps to read more about NestJS.
Implementing Product Feature
NestJS consists of at least one module – a root module. It is an entry point to the application and is being scaffolded as part of nest new command. To better organise the solution and directory structure, each implemented feature should be contained within a separate module. In this guide, we will create a simple Product module, to manage a list of products.
Scaffolding Necessary Files
We can either create all necessary files following documentation guidance or use Nest CLI. Keeping in mind, that we need to scaffold several files and appropriately register all of them, it is recommended to use the tool. We can create Module, Service and Controller, executing the following commands:
# Scaffold Module, Service and Controller nest generate module products nest generate service products nest generate controller products # or use alias version nest g mo products nest g so products nest g c products
As stated, the tool creates all necessary files and registers a newly created module (within the root module). It is also worth mentioning, that the Products module registers internal dependencies, so Service is injectable and Controller reachable.
Below, we can see an output of freshly created files:
// products.module.ts import { Module } from '@nestjs/common'; import { ProductsService } from './products.service'; import { ProductsController } from './products.controller'; @Module({ providers: [ProductsService], controllers: [ProductsController], }) export class ProductsModule {} // products.service.ts import { Injectable } from '@nestjs/common'; @Injectable() export class ProductsService {} // products.controller.ts import { Controller } from '@nestjs/common'; @Controller('products') export class ProductsController {}
It is worth noticing the use of different decorators, declared within the @nestjs namespace. The framework leverages the latest features of TypeScript to better describe specific parts of code and make it more readable. Decorators such as @Module, @Controller or @Injectable are commonly used across multiple frameworks – this makes overall syntax easier to understand.
Implementing NestJS REST API
Once the solution and module scaffolding is complete, we can finally start implementing functionality to manage products. For the sake of simplicity, we will use an in-memory collection, so there is no need to add external dependencies. NestJS does not imply any specific data layer implementation, however, we can find some useful information on how to integrate more common providers.
Let’s create a couple of files first, which we will use in our REST API:
- product.ts – our Product model, we will use it for GET requests
- create-update-product-dto.ts – our data transfer object to create and update products; we will use it as a request body for POST and PUT requests
// products.ts export class Product { id: string; name: string; description: string; price: number; } // create-update-product-dto.t export class CreateUpdateProductDto { name: string; description: string; price: number; }
At this point, models don’t stand out in any way – these are regular TypeScript classes. We will revisit them in the next section when we introduce OpenAPI support.
Next, we need to define service and controller. Again, for the sake of simplicity, we will operate on the collection in memory. An example implementation may look like this:
// products.service.ts import { Injectable } from '@nestjs/common'; import { Product } from './interfaces/product'; import { CreateProductDto } from './interfaces/create-product-dto'; import { UpdateProductDto } from './interfaces/update-product-dto'; import { v4 as uuidv4 } from 'uuid'; @Injectable() export class ProductsService { private products: Product[] = []; findAll(): Product[] { return this.products; } findById(id: string): Product { return this.products.find((x) => x.id === id); } create(createProductDto: CreateProductDto): Product { const product = { id: uuidv4(), ...createProductDto, }; this.products.push(product); return product; } update(id: string, updateProductDto: UpdateProductDto): Product { const productIndex = this.products.findIndex((x) => x.id === id); const product = { id: id, ...updateProductDto, }; this.products[productIndex] = product; return product; } delete(id: string): void { this.products = this.products.filter((x) => x.id !== id); } }
Note: In this example, we use uuid package to generate GUID. The package can be installed using the command npm install uuid.
// products.controller.ts import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; import { ProductsService } from './products.service'; import { Product } from './interfaces/product'; import { CreateUpdateProductDto } from './interfaces/create-update-product-dto'; @Controller('products') export class ProductsController { constructor(private readonly productsService: ProductsService) { } @Get() findAll(): Product[] { return this.productsService.findAll(); } @Get(':id') findById(@Param('id') id: string): Product { return this.productsService.findById(id); } @Post() create(@Body() createUpdateProductDto: CreateUpdateProductDto): Product { return this.productsService.create(createUpdateProductDto); } @Put(':id') update(@Param('id') id: string, @Body() createUpdateProductDto: CreateUpdateProductDto): Product { return this.productsService.update(id, createUpdateProductDto); } @Delete(':id') delete(@Param('id') id: string): void { return this.productsService.delete(id); } }
Our application should now be able to respond to API requests, i.e., by visiting http://localhost:3000/products in the browser, we should see an empty JSON array.
Adding Open API Support
It is worth covering an important aspect of building a RESTful API, which is standardization. We can observe a trend in the industry, where inter-service communication is more frequent. Even without committing fully to architectures such as Microservices, it is a common practice to delegate specific tasks to an external service. The use of standards such as OpenAPI helps create technology-agnostic services and abstract implementation details of the service from the API consumer.
NestJS provides integration with OpenAPI tools, to generate OpenAPI 3.0 specification in (JSON or YAML format), as well as provides a graphical user interface – Swagger UI. To get started we need to install the following package and register it within the main.ts file:
# Install @nestjs/swagger package npm install --save @nestjs/swagger
// main.ts import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); // Add configuration below const config = new DocumentBuilder() .setTitle('NestJS Sample App') .setVersion('1.0') .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); await app.listen(3000); } bootstrap();
Having these changes in place, we can visit http://localhost:3000/api to open Swagger UI. At this stage, documentation may be incomplete or even empty. This is due to the lack of appropriate decorators within the API layer. We need to revise previously created components (model and controller) to include the necessary annotations:
// products.ts import { ApiProperty } from '@nestjs/swagger'; export class Product { @ApiProperty() id: string; @ApiProperty() name: string; @ApiProperty() description: string; @ApiProperty() price: number; } // create-update-product-dto.t import { ApiProperty } from '@nestjs/swagger'; export class CreateUpdateProductDto { @ApiProperty() name: string; @ApiProperty() description: string; @ApiProperty() price: number; }
Note: At a minimum, we need to specify @ApiProperty() decorator. We can pass an optional object to better describe the property, i.e., description, type or example value. Visit https://docs.nestjs.com/openapi/types-and-parameters to learn more about available decorators.
// products.controller.ts import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; import { ProductsService } from './products.service'; import { Product } from './interfaces/product'; import { CreateUpdateProductDto } from './interfaces/create-update-product-dto'; import { ApiCreatedResponse, ApiNoContentResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger'; @Controller('products') @ApiTags('products') export class ProductsController { constructor(private readonly productsService: ProductsService) { } @Get() @ApiOkResponse({ type: [Product], description: 'Retrieve all Products', }) findAll(): Product[] { return this.productsService.findAll(); } @Get(':id') @ApiOkResponse({ type: Product, }) findById(@Param('id') id: string): Product { return this.productsService.findById(id); } @Post() @ApiCreatedResponse({ type: Product, }) create(@Body() createUpdateProductDto: CreateUpdateProductDto): Product { return this.productsService.create(createUpdateProductDto); } @Put(':id') @ApiOkResponse({ type: Product, }) update(@Param('id') id: string, @Body() createUpdateProductDto: CreateUpdateProductDto): Product { return this.productsService.update(id, createUpdateProductDto); } @Delete(':id') @ApiNoContentResponse() delete(@Param('id') id: string): void { return this.productsService.delete(id); } }
Note: The more details we provide to describe API methods, the more comprehensive the OpenAPI specification. It is worth adding decorators such as @ApiOkResponse() to indicate, that the expected response HTTP status code should be OK (200). Visit https://docs.nestjs.com/openapi/operations to learn more about available decorators.
With these changes in place, we should be now able to see the following user interface:
API Overview – 5 endpoints available; 2 models defined
Products Endpoint – Expected response type and content
Model Definitions
Tips for Adding Open API Support
It is worth mentioning that we can access the OpenAPI specification using the URL: http://localhost:3000/api-json. This specification can be used to:
- Generate testing collections – we can import specifications into tools such as Postman. As a result, we’ll get access to all endpoints and models (the more information we provided when describing our model and controller, the more details will be available).
- Generate client SDKs – we can use various tools to generate strongly-typed API clients. This helps consuming the API, as generators will create proxy services to call specific endpoints.
Testing the API
To test our API, we will create a couple of products – Orange Juice and White Bread. We’ll then query, update and delete one of them. In these examples, we use PowerShell to execute all requests.
Request 1 – POST – Create Orange Juice:
# Prepare body $body = @{name="Orange Juice"; description="Orange Juice with Bits 1L"; price=2.99} | ConvertTo-Json # Send request $response = Invoke-WebRequest -Uri http://localhost:3000/products -Method POST -Body $body -ContentType "application/json" # Display response content $response.Content | ConvertFrom-Json
Request 2 – POST – Create White Bread:
# Prepare body $body = @{name="White Bread"; description="Soft White Medium Bread 800g"; price=1.49} | ConvertTo-Json # Send request $response = Invoke-WebRequest -Uri http://localhost:3000/products -Method POST -Body $body -ContentType "application/json" # Display response content $response.Content | ConvertFrom-Json
Request 3 – GET- Retrieve all products (array):
# Send request $response = Invoke-WebRequest -Uri http://localhost:3000/products -Method GET # Display response content $response.Content | ConvertFrom-Json
Request 4 – GET- Retrieve single product with id 6ba0a452-d7da-49fe-bada-6527122baeee:
# Send request $response = Invoke-WebRequest -Uri http://localhost:3000/products/6ba0a452-d7da-49fe-bada-6527122baeee -Method GET # Display response content $response.Content | ConvertFrom-Json
Request 5 – PUT- Update Orange Juice:
# Prepare body $body = @{name="Orange Juice"; description="Orange Juice with Bits 1.5L"; price=3.49} | ConvertTo-Json # Send request $response = Invoke-WebRequest -Uri http://localhost:3000/products/86c9a89e-39ac-4724-8df5-fa7471c26990 -Method PUT -Body $body -ContentType "application/json" # Display response content $response.Content | ConvertFrom-Json
Request 6 – PUT- Delete Orange Juice:
# Send request $response = Invoke-WebRequest -Uri http://localhost:3000/products/86c9a89e-39ac-4724-8df5-fa7471c26990 -Method DELETE # Outputs # No content
Next Steps
We can now focus on the next steps – extending the functionality and some improvements, such as the introduction of a persistence layer, authentication or cross-cutting concerns. As this guide focuses on the API, these aspects won’t be covered in this post. Instead, we will briefly look into the deployment.
Application Build and Deployment
Prerequisites: Make sure that Azure CLI is installed on your operating system.
To deploy a NestJS application to Microsoft Azure, we need to build the application first. We can do it, by executing the following command:
# Build Application npm run build
Behind the scenes, the Nest CLI tool will be used to build the application. A new dist directory will be created, which contains a compiled, production-ready bundle. As mentioned at the beginning, NestJS is a Node.js framework, therefore it can be hosted using Azure Web App service. When creating a new Web App service, we need to be sure to select the appropriate runtime stack – which is Node:
Create Web App
Note: It is recommended to use the latest LTS version (currently Node 16 LTS), but older versions can be used as well (version >= 12).
Having both in place (build and Azure Web App service), we should be able to deploy our sample NestJS application. There are many ways of doing that – in our guide, we will use Azure CLI. The application can be deployed, using the following commands:
# Create Archive Compress-Archive -Path dist/* -DestinationPath nestjs-app.zip # Login to Azure az login # Deploy Application az webapp deploy --resource-group <group-name> --name <app-name> --src-path ./nestjs-app.zip
Conclusion
NestJS framework certainly is an interesting offering, as it brings many qualities into the world of Node.js programming. Built on a solid foundation, focuses on patterns and best practices to create clean and manageable code. At the same time, it offers flexibility and lets the developer focus on implementation details. It is a great tool to build robust and scalable REST API.
When to consider NestJS
The NestJS Framework allows you to build various types of server-side applications, where the most common is the REST API. It also offers an arsenal of additional packages to create more comprehensive solutions. Task scheduling, queuing, events or caching – to name a few. Thanks to that, developers can focus on things that matter – business logic, and effectively improving time to market.
Applications created in NestJS have the same shape, apply the same concepts, and follow well-defined patterns. These principles require discipline but result in better code quality. If quality is important (and it always should be), NestJS may be a good fit.
For some teams, it may be beneficial to develop full-stack solutions using Node.js. It is common practice to build multi-tier applications using any combination of front-end and back-end frameworks – frequently these frameworks use different technologies. With NestJS, this difference can be greatly reduced.
Drawbacks
When it comes to concerns, we need to keep in mind, that due to its comprehensiveness and offered functionality, NestJS may be less performant. As mentioned before, the framework is built on top of Express (or Fastity, as the underlying framework can be changed), and adding an additional layer of functionality comes with a cost. Therefore, if performance is a priority, it may be worth using the underlying framework directly.
The framework may have a high entry threshold, especially for developers using weakly-typed languages. NestJS Framework implies certain rules, while the compiler enforces them. It may take some time to learn more advanced features of TypeScript, which the framework makes frequent use of.
Development at Ballard Chalmers
Here at Ballard Chalmers, we usually use .NET for REST API development, so looking at the action from another angle is refreshing. If you would like to get in touch about your software development, whether you are looking to outsource a full project, or need resourcing or consultancy, why not get in touch?