Nest.js is well known for creating extensive and progressive APIs. It helps you create APIs that are very easy to test, scale, and maintain.
To achieve this, it is well structured with three main building blocks. These are:
Modules - Help you organize your code into reusable units to organize your application structure comfortably.
Providers/services - They abstract application complexity.
Controllers - Used to handle the API HTTP routing mechanisms. Controllers allow you to comfortably handle incoming requests and return the appropriate responses to the client.
To demonstrate how to build a complex Nest.js API, we will build an eCommerce API that handles products and orders. This API can also include the authentication module. Check out this guide and learn how to implement authentication in Nest.js. Alternatively, you can check the complete code on this GitHub repository that contains the authentication, products, and orders modules.
To continue with this article, it is helpful to have the following:
Node.js installed on your computer.
MongoDB installed on your computer.
Postman installed on your computer.
Prior experience working with Nest.js.
To create a progressive Nest.js project, first ensure you have the Nest.js CLI installed:nest --version
If you do not have the Nest.js CLI installed, run the following command and install it on your computer:npm i -g @nestjs/cli
Create your Nest.js project using the following command:nest new eCommerce-api
Once done, go to the created Nest.js project:cd ecommerce -api
Install the database dependencies that we will use in this guide:npm i --save @nestjs/mongoose
Inside the src/models
directory, create a product.schema.ts
file. In it, define the product schema as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// Import mongoose to create the db Schema import * as mongoose from 'mongoose'; // Use the Schema() from mongoose to define your db Schema // Also, export the Schema to access it within the project export const productSchema = new mongoose.Schema({ // Details for the database info to be created for each product // A title field title: String, // A description field description: String, // An image URL image: String, // The price price: String, // Dates fields createdAt: { // timestamp // takes date type type: Date, // the field is created by default using the current timestamp default: Date.now } });
Inside the src/types
directory, create a product.ts
file. Inside it, define the product as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// Add mongoose Document for types checks import { Document } from 'mongoose'; // Create and export an interface for the product document export interface Product extends Document { // Add the fields from your schema // Each field and its type title: string, description: string, image: string, price: string, createdAt: Date }
Inside the src
directory, create a products
folder. Inside the folder, create a products.dto.ts
file. The file will host the interfaces based on the input data.
Inside the file, add the following definitions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// create an interfaces for the data objects export interface ProductDTO { // product payload and the necessary fields // Note: Date is not added as it is created by ddefault title: string, description: string, image: string, price: string, } // export another DTO interface for handling IDS export interface ProductIdDTO { // Here, add the product id to the payload id: string, }
Inside the src/products
folder, create a products.service.ts
file. The file will host all the operations to be done on a product.
Inside the file:
Import the necessary modules:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; // Model from mongoose import { Model } from 'mongoose'; // Import mongoose from nest to execute the database from your project import { InjectModel } from '@nestjs/mongoose'; // Import the types import { Product } from 'src/types/product'; // Import the DTO interfaces import { ProductDTO, ProductIdDTO } from './products.dto';
Define a ProductsService class:
1
2
3
4
5
@Injectable()
export class ProductsService {
// define a constructor to inject the model into the service
constructor(@InjectModel('Product') private readonly productModel: Model < Product > ) {}
}
Inside the class, define a method for creating a new product:
1 2 3 4 5 6 7 8 9
// create a new product and add to the database async create(product: ProductDTO): Promise < Product > { // create a product from the input details const createdProduct = new this.productModel(product); // save the product to your database await createdProduct.save(); // return the saved product return createdProduct; }
Inside the class, define a method for getting all products:
1 2 3 4 5 6
// getting all products from the db async findAll(): Promise < Product[] > { // Get and return all products added to your db return await this.productModel.find() .exec(); }
Inside the class, define a method for getting a product by ID:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// getting product by id async findById(productId: ProductIdDTO): Promise < Product > { // get Id from the input for specific items const { id } = productId; try { // Get and return the product from the db based on id return await this.productModel.findById(id) .exec(); } catch (error) { // In case of an error, return it as the server error throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR); } }
Inside the class, define a method for updating a product:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// updating a product as existing db async update(productId: ProductIdDTO, product: ProductDTO): Promise < Product > { // get Id from the input for updating details const { id } = productId; try { // Update the product and return it return await this.productModel.findByIdAndUpdate(id, product, { new: true }) .exec(); } catch (error) { // In case of an error, return it throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR); } }
Inside the class, define a method for deleting a product:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// deleting a product from the db based on id async delete(productId: ProductIdDTO): Promise < any > { // get Id from the input for deleting it const { id } = productId; try { // Delete the product and return it. return await this.productModel.findByIdAndRemove(id) .exec(); } catch (error) { // In case of an error, return it. throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR); } }
Inside the src/products
folder, add a products.controller.ts
file. The file will host all the product routes and their respective functionalities.
Inside the file:
Import the necessary modules:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
import { Body, Controller, Post, Get, Query } from '@nestjs/common'; import { ProductsService } from './products.service'; import { ProductDTO, ProductIdDTO } from './products.dto';
Define the ProductsController class:
1
2
3
4
5
@Controller('products')
export class ProductsController {
// define the products service
constructor(private productsService: ProductsService) {}
}
Inside the class, define the route to get all products:
1 2 3 4 5
@Get() async findAll() { let all_products = await this.productsService.findAll(); // Get all products return all_products; // return the products }
Inside the class, define the route to get a specific product by ID:
1 2 3 4 5
@Get('/:id') async findById(@Query() productId: ProductIdDTO) { let product = await this.productsService.findById(productId); // Get a product by Id return product; // return the product }
Inside the class, define the route to create a product:
1 2 3 4 5
@Post('/create') async create(@Body() product: ProductDTO) { let created_product = await this.productsService.create(product); // create product return created_product; // return the created product }
Inside the class, define the route to update a product:
1 2 3 4 5
@Post('/update') async update(@Query() productId: ProductIdDTO, @Body() product: ProductDTO) { let updated_product = await this.productsService.update(productId, product); // Update the product return updated_product; // return the updated product }
Inside the class, define the route to delete a product:
1 2 3 4 5
@Post('/delete') async delete(@Query() productId: ProductIdDTO) { let deleted_product = await this.productsService.delete(productId); // Delete the product return deleted_product; // return the deleted product }
Inside the src/products
directory, create a products.module.ts
file. The file will host the definition of the product module in terms of its imports
, controllers
, providers
, and exports
.
Inside the file:
Import the necessary modules:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import { Module } from '@nestjs/common'; // add the controller import { ProductsController } from './products.controller'; // add the mongoose ORM import { MongooseModule } from '@nestjs/mongoose'; // add the service import { ProductsService } from './products.service'; // add the mongoose schema import { productSchema } from 'src/models/product.schema';
Define the module:
1 2 3 4 5 6 7 8 9 10 11 12 13
@Module({ // set the schema imports: [MongooseModule.forFeature([{ name: 'Product', schema: productSchema }])], // add the controller module controllers: [ProductsController], // add the providers module providers: [ProductsService], // exports the service module exports: [ProductsService] })
Define and export the ProductsModule class:
1
export class ProductsModule {}
On src/app.module.ts
, add the following changes:
Add an import for ProductsController, and ProductsModule:
1 2 3 4 5 6 7 8
// import controller import { ProductsController } from 'src/products/products.controller'; // import the module import { ProductsModule } from 'src/products/products.module';
In module definition, under imports, add ProductsModule, also under controllers, add ProductsController as follows:
1 2 3 4 5 6 7 8 9 10 11
@Module({ imports: [ // add the connection URL to your MongoDB database MongooseModule.forRoot('mongodb://localhost/ecommerce'), SharedModule, // execute the module ProductsModule, ], controllers: [AppController, ProductsController], providers: [AppService], })
Inside src/models
. Create an order.schema.ts
file. Inside the file, define the orders schema as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import * as mongoose from 'mongoose';
// create and export the schema for handling the orders
export const orderSchema = new mongoose.Schema({
totalPrice: {
type: Number,
required: true
},
products: [
{
product: { // Products reference
type: mongoose.Schema.Types.ObjectId,
ref: 'Product'
},
// define the quantity of the orders made
quantity: {
type: Number,
default: 0
}
}
],
// define the date order was created
createdAt: {
type: Date,
default: Date.now
}
});
Inside the src/types
directory, create an order.ts
file. Inside the file, define the order as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
import { Document } from 'mongoose'; import { Product } from './product'; interface ProductOrder { product: Product, quantity: number } export interface Order extends Document { totalPrice: number, createdAt: Date, products: ProductOrder[] }
Inside the src
folder, create an orders
directory. Inside the directory, create an orders.dto.ts
file. The file will host the definition of interfaces for input data from the client.
Inside the orders.dto.ts
file, add the following definitions:
1 2 3 4 5 6 7 8 9 10
export interface OrderDTO { totalPrice: number, product: string, quantity: number } export interface OrderIdDTO { id: string }
Inside the src/orders
directory, create an orders.service.ts
file. The file will host all the operations concerning an order.
Inside the file:
Import the necessary modules:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { Order } from 'src/types/order'; import { OrderDTO, OrderIdDTO } from './orders.dto'; import { Model } from 'mongoose'; // add mongoose import { InjectModel } from '@nestjs/mongoose';
Define the OrdersService class:
1
2
3
4
5
@Injectable()
export class OrdersService {
// define a constructor to inject the model into the service
constructor(@InjectModel('Order') private readonly orderModel: Model < Order > ) {}
}
Inside the class, define a method for creating new order:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// create a new order async create(order: OrderDTO): Promise < Order > { // Get data from input and structure it. let new_order = { totalPrice: order.totalPrice, products: [ { product: order.product, quantity: order.quantity } ] } const createdOrder = new this.orderModel(new_order); // Create the order. await createdOrder.save(); // Save the order. return createdOrder; // return the saved order. }
Inside the class, define a method for getting all orders:
1 2 3 4 5 6
// getting all orders async findAll(): Promise < Order[] > { // get and return all orders. return await this.orderModel.find() .exec(); }
Inside the class, define a method for getting an order by ID:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// getting order by id async findById(orderId: OrderIdDTO): Promise < Order > { const { id } = orderId; // Extract the ID try { // Get and return the order return await this.orderModel.findById(id) .exec(); } catch (error) { // In case of an error, show it throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR); } }
Inside the class, define a method for updating an order:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// updating an order async update(orderId: OrderIdDTO, order: OrderDTO): Promise < Order > { const { id } = orderId; // Extract the ID let updated_order = { // Structure the update object ...(order.totalPrice && { totalPrice: order.totalPrice }), } try { return await this.orderModel.findByIdAndUpdate(id, updated_order, { new: true }) .exec(); // Update and return the updated order } catch (error) { throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR); // In case of an error, show it. } }
Inside the class, define a method for deleting an order:
1 2 3 4 5 6 7 8 9 10 11 12
// deleting an order async delete(orderId: OrderIdDTO): Promise < any > { const { id } = orderId; // Extract the ID try { return await this.orderModel.findByIdAndRemove(id) .exec(); // Remove an order and return it. } catch (error) { throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR); // In case of an error, show it. } }
Inside the src/orders
directory, create an orders.controller.ts
file. The file will host all the order’s routes and their respective functionalities.
Inside the file:
Import the necessary modules:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
import { Body, Controller, Post, Get, Query } from '@nestjs/common'; import { OrdersService } from './orders.service'; import { OrderDTO, OrderIdDTO } from './orders.dto';
Create an OrdersController class:
1
2
3
4
5
6
@Controller('orders')
//create and export the controller
export class OrdersController {
// define the orders service
constructor(private ordersService: OrdersService) {}
}
Inside the class, define a route for getting all orders:
1 2 3 4 5
@Get() async findAll() { let all_orders = await this.ordersService.findAll(); // get all orders return all_orders; }
Inside the class, define a route for getting a specific order:
1 2 3 4 5
@Get('/:id') async findById(@Query() orderId: OrderIdDTO) { let order = await this.ordersService.findById(orderId); // get a specific order return order; }
Inside the class, define a route for creating an order:
1 2 3 4 5
@Post('/create') async create(@Body() order: OrderDTO) { let created_order = await this.ordersService.create(order); // Create an order return created_order; }
Inside the class, define a route for updating an order:
1 2 3 4 5
@Post('/update') async update(@Query() orderId: OrderIdDTO, @Body() order: OrderDTO) { let updated_order = await this.ordersService.update(orderId, order); // update the order return updated_order; }
Inside the class, define a route for deleting an order:
1 2 3 4 5
@Post('/delete') async delete(@Query() orderId: OrderIdDTO) { let deleted_order = await this.ordersService.delete(orderId); // delete the orderd return deleted_order; }
Inside the src/orders
directory, create an orders.module.ts
file. The file will host the order’s module definition in terms of its imports
, controllers
, providers
, and exports
.
Inside the file:
Import the necessary modules:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
import { Module } from '@nestjs/common'; import { orderSchema } from 'src/models/order.schema'; import { OrdersController } from './orders.controller'; import { OrdersService } from './orders.service'; import { MongooseModule } from '@nestjs/mongoose';
Define the module:
1 2 3 4 5 6 7 8 9 10 11 12
@Module({ imports: [MongooseModule.forFeature([{ name: 'Order', schema: orderSchema }])], // execute the orders controller controllers: [OrdersController], // execute the orders provider/service providers: [OrdersService], // export the orders service exports: [OrdersService] })
Define and export the OrdersModule
class:
1
export class OrdersModule {}
On src/app.module.ts
file, add the following changes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
Import OrdersModule and OrdersController: import { OrdersModule } from 'src/orders/orders.module'; import { OrdersController } from 'src/orders/orders.controller'; `` ` In the module definition, under imports, add the ` OrdersModule`, and then under controllers add the OrdersController as follows: ` `` js @Module({ imports: [ MongooseModule.forRoot('mongodb://localhost/ecommerce'), SharedModule, ProductsModule, OrdersModule, ], controllers: [AppController, ProductsController, OrdersController], providers: [AppService], })
At this point, we have done all the steps. Ensure that the development server is running and proceed to test the app.
Note: An extensive API like request user authentication. Check the GitHub repository for all the code used in this guide and the user authentication part. This will allow you to have protected routes that log the owner/user of a given operation.
On a new tab on the postman, send a POST request to: http://localhost:3000/products/create
. Under the body section, add the following raw JSON data:
1 2 3 4 5
{ "title": "Product one", "description": "Good product one.", "image": "Your Image URL" }
Then hit Send
. Your response should be similar to:
Feel free to add as many products as you can.
After that, send a GET request to http://localhost:3000/products/
.
Your response should be similar to:
Your response will be based on the number of products you have added.
Feel free to test out all the other routes.
At this stage, we have completed the steps.
Ensure that your development server is running.
Launch a separate tab on postman and send a request (POST) to: http://localhost:3000/orders/create
:
Under the body section, include the following raw JSON data:
1 2 3 4 5
{ "product": "your_product_id", "quantity": "1", "totalPrice": "500" }
On the above JSON object, replace your_product_id
with any _id
of any product you created earlier. Then hit Send
. Your response should be similar to:
Feel free to create as many orders as you can.
After that, send a GET request to http://localhost:3000/orders/
:
Based on the orders you have added, your response should be similar to:
Feel free to test all the other routes.