Building a REST API with NestJS (2025 Guide)


In this tutorial, I will show you how to develop a basic REST API using NestJS.

Requirements review

Now, suppose you are given the following database design:

Database diagram

You are required to create a REST API with the following endpoints:

Products:

MethodEndpointDescription
GET/api/productsGet paginated list of products
GET/api/products/{id}Get a specific product by ID
POST/api/productsCreate a new product
PUT/api/products/{id}Update an existing product
DELETE/api/products/{id}Delete a product

Categories:

MethodEndpointDescription
GET/api/categoriesGet paginated list of categories
GET/api/categories/{id}Get a specific category by ID
POST/api/categoriesCreate a new category
PUT/api/categories/{id}Update an existing category
DELETE/api/categories/{id}Delete a category

Example of responses: GET /api/products

{
  "content": [
    {
      "id": 1,
      "name": "Smartphone X",
      "description": "A high-end smartphone with an excellent camera.",
      "price": 999.99,
      "categories": [
        {
          "id": 2,
          "name": "Electronics"
        },
        {
          "id": 5,
          "name": "Mobile Phones"
        }
      ]
    }
  ],
  "pageNo": 0,
  "pageSize": 10,
  "totalElements": 1,
  "totalPages": 1,
  "last": true
}

GET /api/products/{id}

{
  "id": 1,
  "name": "Smartphone X",
  "description": "A high-end smartphone with an excellent camera.",
  "price": 999.99,
  "categories": [
    {
      "id": 2,
      "name": "Electronics"
    },
    {
      "id": 5,
      "name": "Mobile Phones"
    }
  ]
}

POST /api/products

// Body
{
  "name": "Smartwatch Z",
  "description": "A waterproof smartwatch with fitness tracking.",
  "price": 199.99,
  "categories": [2, 7]
}

// Response
{
  "id": 2,
  "name": "Smartwatch Z",
  "description": "A waterproof smartwatch with fitness tracking.",
  "price": 199.99,
  "categories": [
    {
      "id": 2,
      "name": "Electronics"
    },
    {
      "id": 7,
      "name": "Wearables"
    }
  ]
}

PUT /api/products/{id}

// Body
{
  "name": "Smartwatch Z Pro",
  "description": "Upgraded smartwatch with longer battery life.",
  "price": 249.99,
  "categories": [2, 7]
}

// Response
{
  "id": 2,
  "name": "Smartwatch Z Pro",
  "description": "Upgraded smartwatch with longer battery life.",
  "price": 249.99,
  "categories": [
    {
      "id": 2,
      "name": "Electronics"
    },
    {
      "id": 7,
      "name": "Wearables"
    }
  ]
}

DELETE /api/products/{id}

{
  "message": "Product deleted successfully"
}

The database must be implemented using PostgreSQL.

Database

If you don’t have a PostgreSQL database, install Docker on your computer and use the file docker-compose.local-dev.yaml to create a PostgreSQL server and a database.

Add a file .env in the root of the project with the following content:

# App
PORT=3000
CLIENT_URL="http://localhost:5173"

# DB Postgress
POSTGRES_DB_NAME=products-api
POSTGRES_DB_HOST=localhost
POSTGRES_DB_PORT=5432
POSTGRES_DB_USERNAME=admin
POSTGRES_DB_PASSWORD=admin

Then run this command in the root of the project:

docker compose -f docker-compose.local-dev.yaml up -d

Start coding

Project setup

Install NestJS:

npm i -g @nestjs/cli

Create the project with this command:

nest new products-api

Select the package manager you want to use, I will use npm:

Which package manager would you ❤️ to use? npm

This command will create a folder products-api with a minimal NestJS project.

Now we can install the dependencies:

npm ci

And run the project:

npm run start

The app will be up and running on port 3000.

You can test the endpoints using Postman or the REST Client extension in VSCode

I will use the REST Client extension:

Image description

In the root of the project add a folder rest-client. Inside it add a file products.http and place this content:

@base_url=http://localhost:3000

# Get all products

GET {{base_url}}

You will see something like this:

Image description

Project directory structure

Create the following folders inside src:

  • core
  • database
  • products
  • categories

Project configuration

Database

Install the following packages:

npm install @nestjs/typeorm typeorm pg @nestjs/config
  • pg: Driver for communicating our NestJS app with the PostgreSQL database.
  • typeorm: Object Relational Mapper (ORM) for TypeScript.
  • @nestjs/typeorm: Package provided by NestJS to integrate TypeORM in our app.
  • @nestjs/config: Package provided by NestJS to use environment variables.

Create the following files inside the folder /database/ and paste the content:

/entities/base.ts:

import { CreateDateColumn, DeleteDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";

export abstract class BaseModel {
  @PrimaryGeneratedColumn()
  id: number;

  @CreateDateColumn({
    name: "created_at",
    type: "timestamptz",
    default: () => "CURRENT_TIMESTAMP",
  })
  createdAt: Date;

  @UpdateDateColumn({
    name: "updated_at",
    type: "timestamptz",
    default: () => "CURRENT_TIMESTAMP",
    onUpdate: "CURRENT_TIMESTAMP",
  })
  updatedAt: Date;

  @DeleteDateColumn({
    name: "deleted_at",
    type: "timestamptz",
  })
  deletedAt: Date;
}

/entities/product.ts:

import { BaseModel } from "@src/database/entities/base";
import { Entity, Column, ManyToMany, JoinTable } from "typeorm";
import { Category } from "./category";

@Entity({ name: "products" })
export class Product extends BaseModel {
  @Column()
  name: string;

  @Column()
  description: string;

  @Column("decimal", { precision: 10, scale: 2 })
  price: number;

  @ManyToMany(() => Category, (category) => category.products, {
    cascade: true,
  })
  @JoinTable({
    name: "product_categories",
    joinColumn: { name: "product_id", referencedColumnName: "id" },
    inverseJoinColumn: { name: "category_id", referencedColumnName: "id" },
  })
  categories: Category[];
}

/entities/category.ts:

import { BaseModel } from "@src/database/entities/base";
import { Entity, Column, ManyToMany } from "typeorm";
import { Product } from "./product";

@Entity({ name: "categories" })
export class Category extends BaseModel {
  @Column()
  name: string;

  @ManyToMany(() => Product, (product) => product.categories)
  products: Product[];
}

/providers/postgresql.provider.ts:

import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from "@nestjs/typeorm";
import { Product } from "@src/database/entities/product";
import { Category } from "../entities/category";

@Injectable()
export class PostgresqlDdProvider implements TypeOrmOptionsFactory {
  constructor(private readonly configService: ConfigService) {}

  createTypeOrmOptions(): Promise<TypeOrmModuleOptions> | TypeOrmModuleOptions {
    return {
      type: "postgres",
      host: this.configService.get<"string">("POSTGRES_DB_HOST"),
      port: parseInt(this.configService.get<"string">("POSTGRES_DB_PORT") ?? "5432"),
      username: this.configService.get<"string">("POSTGRES_DB_USERNAME"),
      password: this.configService.get<"string">("POSTGRES_DB_PASSWORD"),
      database: this.configService.get<"string">("POSTGRES_DB_NAME"),
      entities: [Product, Category],
      synchronize: true,
      logging: false,
    };
  }
}

/database.module.ts:

import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";

import { PostgresqlDdProvider } from "./providers/postgresql.provider";

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      name: "postgres", // Explicitly set the connection name for PostgreSQL
      useClass: PostgresqlDdProvider,
    }),
  ],
  exports: [TypeOrmModule],
})
export class DatabaseModule {}

Core

Delete files:

  • app.service.ts
  • app.controller.ts
  • app.controller.spec.ts

Install the following packages:

npm install helmet nestjs-pino @nestjs/throttler
npm install pino-pretty --save-dev
  • helmet: Middleware that enhances security by setting various HTTP headers.
  • nestjs-pino: Logging integration for NestJS that uses the pino logger. Pino is a fast and efficient logging library for Node.js.
  • @nestjs/throttler: NestJS module that provides rate-limiting functionality for your application.
  • pino-pretty: Formats Pino’s structured JSON logs into a human-readable, colorized output for easier debugging in development.

Replace the content in main.ts with the following:

import { NestFactory } from "@nestjs/core";
import { ValidationPipe, Logger } from "@nestjs/common";

import { Logger as PinoLogger } from "nestjs-pino";
import helmet from "helmet";

import { AppModule } from "./app.module";

import * as bodyParser from "body-parser";

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    bufferLogs: true,
    bodyParser: true,
  });

  // Increase payload size limit
  app.use(bodyParser.json({ limit: "10mb" }));
  app.use(bodyParser.urlencoded({ limit: "10mb", extended: true }));

  app.useLogger(app.get(PinoLogger));

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    })
  );

  app.use(helmet());

  app.enableCors({
    origin: process.env.CLIENT_URL ?? "*",
    credentials: true,
  });

  await app.listen(process.env.PORT ?? 3000);

  const logger = new Logger("Bootstrap");

  logger.log(`App is running on ${await app.getUrl()}`);
}

bootstrap();

Replace the content in app.module.ts with the following:

import { Module } from "@nestjs/common";

import { CoreModule } from "./core/core.module";
import { ProductsModule } from "./products/products.module";
import { CategoriesModule } from "./categories/categories.module";

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

Create the following file inside the folder /core/ and paste the content:

/core/core.module.ts:

import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { ThrottlerModule } from "@nestjs/throttler";

import { LoggerModule } from "nestjs-pino";

import { DatabaseModule } from "@src/database/database.module";

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),

    ThrottlerModule.forRoot([
      {
        ttl: 60000, // Time-to-live in milliseconds
        limit: 60, // Maximum requests per window globally
      },
    ]),

    LoggerModule.forRoot({
      pinoHttp: {
        serializers: {
          req: () => undefined,
          res: () => undefined,
        },
        autoLogging: false,
        level: process.env.NODE_ENV === "production" ? "info" : "debug",
        transport:
          process.env.NODE_ENV === "production"
            ? undefined
            : {
                target: "pino-pretty",
                options: {
                  messageKey: "message",
                  colorize: true,
                },
              },
        messageKey: "message",
      },
    }),

    DatabaseModule,
  ],
})
export class CoreModule {}

Products

Install the following packages:

npm install class-validator class-transformer
  • class-validator: Package that provides decorators and functions to validate the properties of classes, ensuring that the data meets specified rules. I will use it in DTOs.
  • class-transformer: Transforms plain JavaScript objects into class instances and vice versa, enabling serialization and deserialization in TypeScript.

Create the following files inside the folder /products/ and paste the content:

/dtos/create-product.dto.ts:

import { ArrayNotEmpty, IsArray, IsNumber, IsString } from "class-validator";

export class CreateProductDto {
  @IsString()
  name: string;

  @IsString()
  description: string;

  @IsNumber()
  price: number;

  @IsArray()
  @ArrayNotEmpty()
  @IsNumber({}, { each: true })
  categories: number[];
}

/dtos/update-product.dto.ts:

import { IsArray, IsNumber, IsOptional, IsString } from "class-validator";

export class UpdateProductDto {
  @IsOptional()
  @IsString()
  name?: string;

  @IsOptional()
  @IsString()
  description?: string;

  @IsOptional()
  @IsNumber()
  price?: number;

  @IsOptional()
  @IsArray()
  @IsNumber({}, { each: true })
  categories?: number[];
}

/dtos/product-response.dto.ts:

export class ProductResponseDto {
  id: number;
  name: string;
  description: string;
  price: number;
  categories: CategoryResponseDto[];
}

export class CategoryResponseDto {
  id: number;
  name: string;
}

/products.mapper.ts:

import { Injectable } from "@nestjs/common";
import { Product } from "@src/database/entities/product";

import { ProductResponseDto } from "./dtos/product-response.dto";

@Injectable()
export class ProductMapper {
  mapEntityToDto(product: Product): ProductResponseDto {
    return {
      id: product.id,
      name: product.name,
      description: product.description,
      price: product.price,
      categories:
        product.categories?.map((category) => ({
          id: category.id,
          name: category.name,
        })) || [],
    };
  }
}

/products.service.ts:

import { BadRequestException, HttpException, Injectable, InternalServerErrorException, Logger, NotFoundException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";

import { In, Repository } from "typeorm";

import { Product } from "@src/database/entities/product";
import { CreateProductDto } from "./dtos/create-product.dto";
import { UpdateProductDto } from "./dtos/update-product.dto";
import { Category } from "@src/database/entities/category";
import { ProductMapper } from "./products.mapper";

@Injectable()
export class ProductsService {
  private readonly logger = new Logger("ProductsService");

  constructor(
    @InjectRepository(Product, "postgres")
    private readonly productRepository: Repository<Product>,
    @InjectRepository(Category, "postgres")
    private readonly categoryRepository: Repository<Category>,
    private readonly productMapper: ProductMapper
  ) {}

  async getAll(pageNo: number = 0, pageSize: number = 10) {
    try {
      // Ensure pageNo is a non-negative integer and pageSize is within reasonable limits
      if (pageNo < 0 || pageSize <= 0) {
        throw new BadRequestException("Invalid page number or page size");
      }

      // Calculate skip value based on page number and page size
      const skip = pageNo * pageSize;

      // Get products with pagination
      const [products, totalElements] = await this.productRepository.findAndCount({
        skip,
        take: pageSize,
        relations: ["categories"], // To load related categories for each product
      });

      // Calculate totalPages and whether it's the last page
      const totalPages = Math.ceil(totalElements / pageSize);
      const last = pageNo + 1 >= totalPages;

      // Map the result into the desired format
      return {
        content: products.map((product) => this.productMapper.mapEntityToDto(product)),
        pageNo,
        pageSize,
        totalElements,
        totalPages,
        last,
      };
    } catch (error) {
      this.handleError(error, "An error occurred while getting products");
    }
  }

  async getById(id: number) {
    try {
      const product = await this.productRepository.findOne({
        where: { id },
        relations: ["categories"],
      });

      if (!product) {
        throw new NotFoundException("Product not found");
      }

      return this.productMapper.mapEntityToDto(product);
    } catch (error) {
      this.handleError(error, "An error occurred while fetching product");
    }
  }

  async create(createProductDto: CreateProductDto) {
    try {
      // Convert category IDs to actual Category entities
      const categories = await this.categoryRepository.findBy({
        id: In(createProductDto.categories),
      });

      if (!categories.length) {
        throw new BadRequestException("Invalid category IDs");
      }

      // Create a new product with the categories attached
      const newProduct = this.productRepository.create({
        ...createProductDto,
        categories,
      });

      await this.productRepository.save(newProduct);
      return this.productMapper.mapEntityToDto(newProduct);
    } catch (error) {
      this.handleError(error, "An error occurred while creating product");
    }
  }

  async update(id: number, updateProductDto: UpdateProductDto) {
    try {
      const { categories, ...updateProductDtoWithoutCategories } = updateProductDto;
      // Find the product by ID and preload with the updated values
      const product = await this.productRepository.preload({
        id,
        ...updateProductDtoWithoutCategories,
      });

      if (!product) {
        throw new NotFoundException("Product not found");
      }

      // If the update involves categories, convert category IDs to actual Category entities
      if (updateProductDto.categories) {
        const categories = await this.categoryRepository.findBy({
          id: In(updateProductDto.categories),
        });

        if (!categories.length) {
          throw new BadRequestException("Invalid category IDs");
        }

        product.categories = categories;
      }

      await this.productRepository.save(product);
      return this.productMapper.mapEntityToDto(product);
    } catch (error) {
      this.handleError(error, "An error occurred while updating product");
    }
  }

  async delete(id: number) {
    try {
      const product = await this.productRepository.findOne({ where: { id } });

      if (!product) {
        throw new NotFoundException("Product not found");
      }

      await this.productRepository.remove(product);
      return { message: "Product deleted successfully" };
    } catch (error) {
      this.handleError(error, "An error occurred while deleting product");
    }
  }

  private handleError(error: unknown, defaultErrorMessage?: string) {
    // Log the error
    this.logger.error(error);

    // Handle known HTTP exceptions
    if (error instanceof HttpException) {
      throw error; // Preserve the original exception, don't modify it
    }

    // Handle unexpected errors
    if (error instanceof Error) {
      // Handle generic errors
      throw new InternalServerErrorException({
        message: defaultErrorMessage ?? "An unexpected error occurred",
      });
    }

    // Default to a generic BadRequestException if error is unknown
    throw new BadRequestException({
      message: defaultErrorMessage ?? "An error occurred in ProductsService",
    });
  }
}

/products.controller.ts:

import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, Query } from "@nestjs/common";

import { ProductsService } from "./products.service";
import { CreateProductDto } from "./dtos/create-product.dto";
import { UpdateProductDto } from "./dtos/update-product.dto";

@Controller("api/products")
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @Get("/")
  getAll(
    @Query("pageNo", new ParseIntPipe({ errorHttpStatusCode: 400 }))
    pageNo: number = 0,
    @Query("pageSize", new ParseIntPipe({ errorHttpStatusCode: 400 }))
    pageSize: number = 10
  ) {
    return this.productsService.getAll(pageNo, pageSize);
  }

  @Get("/:id")
  getById(@Param("id", ParseIntPipe) id: number) {
    return this.productsService.getById(id);
  }

  @Post("/")
  create(@Body() createProductDto: CreateProductDto) {
    return this.productsService.create(createProductDto);
  }

  @Put("/:id")
  update(@Param("id", ParseIntPipe) id: number, @Body() updateProductDto: UpdateProductDto) {
    return this.productsService.update(id, updateProductDto);
  }

  @Delete("/:id")
  delete(@Param("id", ParseIntPipe) id: number) {
    return this.productsService.delete(id);
  }
}

/products.module.ts:

import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Category } from "@src/database/entities/category";
import { Product } from "@src/database/entities/product";
import { ProductsController } from "./products.controller";
import { ProductsService } from "./products.service";
import { ProductMapper } from "./products.mapper";

@Module({
  imports: [TypeOrmModule.forFeature([Product, Category], "postgres")],
  controllers: [ProductsController],
  providers: [ProductsService, ProductMapper],
  exports: [ProductsService],
})
export class ProductsModule {}

Categories

Create the following files inside the folder /categories/ and paste the content:

/dtos/create-category.dto.ts:

import { IsString } from "class-validator";

export class CreateCategoryDto {
  @IsString()
  name: string;
}

/dtos/update-category.dto.ts:

import { IsString } from "class-validator";

export class UpdateCategoryDto {
  @IsString()
  name: string;
}

/dtos/category-response.dto.ts:

export class CategoryResponseDto {
  id: number;
  name: string;
}

/categories.mapper.ts:

import { Injectable } from "@nestjs/common";
import { Category } from "@src/database/entities/category";

import { CategoryResponseDto } from "./dtos/category-response.dto";

@Injectable()
export class CategoryMapper {
  mapEntityToDto(category: Category): CategoryResponseDto {
    return {
      id: category.id,
      name: category.name,
    };
  }
}

/categories.service.ts:

import { BadRequestException, HttpException, Injectable, InternalServerErrorException, Logger, NotFoundException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";

import { Repository } from "typeorm";

import { Category } from "@src/database/entities/category";

import { CreateCategoryDto } from "./dtos/create-category.dto";
import { UpdateCategoryDto } from "./dtos/update-category.dto";
import { CategoryMapper } from "./categories.mapper";

@Injectable()
export class CategoriesService {
  private readonly logger = new Logger("CategoriesService");

  constructor(
    @InjectRepository(Category, "postgres")
    private readonly categoryRepository: Repository<Category>,
    private readonly categoryMapper: CategoryMapper
  ) {}

  async getAll(pageNo: number = 0, pageSize: number = 10) {
    try {
      // Ensure pageNo is a non-negative integer and pageSize is within reasonable limits
      if (pageNo < 0 || pageSize <= 0) {
        throw new BadRequestException("Invalid page number or page size");
      }

      // Calculate skip value based on page number and page size
      const skip = pageNo * pageSize;

      // Get categories with pagination
      const [categories, totalElements] = await this.categoryRepository.findAndCount({
        skip,
        take: pageSize,
      });

      // Calculate totalPages and whether it's the last page
      const totalPages = Math.ceil(totalElements / pageSize);
      const last = pageNo + 1 >= totalPages;

      // Map the result into the desired format
      return {
        content: categories.map((category) => this.categoryMapper.mapEntityToDto(category)),
        pageNo,
        pageSize,
        totalElements,
        totalPages,
        last,
      };
    } catch (error) {
      this.handleError(error, "An error occurred while getting categories");
    }
  }

  async getById(id: number) {
    try {
      const category = await this.categoryRepository.findOne({
        where: { id },
      });

      if (!category) {
        throw new NotFoundException("Category not found");
      }

      return this.categoryMapper.mapEntityToDto(category);
    } catch (error) {
      this.handleError(error, "An error occurred while fetching category");
    }
  }

  async create(createCategoryDto: CreateCategoryDto) {
    try {
      // Create a new category
      const newCategory = this.categoryRepository.create(createCategoryDto);

      await this.categoryRepository.save(newCategory);
      return this.categoryMapper.mapEntityToDto(newCategory);
    } catch (error) {
      this.handleError(error, "An error occurred while creating category");
    }
  }

  async update(id: number, updateCategoryDto: UpdateCategoryDto) {
    try {
      const category = await this.categoryRepository.findOne({
        where: { id },
      });

      if (!category) {
        throw new NotFoundException("Category not found");
      }

      category.name = updateCategoryDto.name;

      await this.categoryRepository.save(category);
      return this.categoryMapper.mapEntityToDto(category);
    } catch (error) {
      this.handleError(error, "An error occurred while updating category");
    }
  }

  async delete(id: number) {
    try {
      const category = await this.categoryRepository.findOne({ where: { id } });

      if (!category) {
        throw new NotFoundException("Category not found");
      }

      await this.categoryRepository.remove(category);
      return { message: "Category deleted successfully" };
    } catch (error) {
      this.handleError(error, "An error occurred while deleting category");
    }
  }

  private handleError(error: unknown, defaultErrorMessage?: string) {
    // Log the error
    this.logger.error(error);

    // Handle known HTTP exceptions
    if (error instanceof HttpException) {
      throw error; // Preserve the original exception, don't modify it
    }

    // Handle unexpected errors
    if (error instanceof Error) {
      // Handle generic errors
      throw new InternalServerErrorException({
        message: defaultErrorMessage ?? "An unexpected error occurred",
      });
    }

    // Default to a generic BadRequestException if error is unknown
    throw new BadRequestException({
      message: defaultErrorMessage ?? "An error occurred in CategoriesService",
    });
  }
}

/categories.controller.ts:

import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, Query } from "@nestjs/common";

import { CategoriesService } from "./categories.service";
import { CreateCategoryDto } from "./dtos/create-category.dto";
import { UpdateCategoryDto } from "./dtos/update-category.dto";

@Controller("api/categories")
export class CategoriesController {
  constructor(private readonly categoriesService: CategoriesService) {}

  @Get("/")
  getAll(
    @Query("pageNo", new ParseIntPipe({ errorHttpStatusCode: 400 }))
    pageNo: number = 0,
    @Query("pageSize", new ParseIntPipe({ errorHttpStatusCode: 400 }))
    pageSize: number = 10
  ) {
    return this.categoriesService.getAll(pageNo, pageSize);
  }

  @Get("/:id")
  getById(@Param("id", ParseIntPipe) id: number) {
    return this.categoriesService.getById(id);
  }

  @Post("/")
  create(@Body() createCategoryDto: CreateCategoryDto) {
    return this.categoriesService.create(createCategoryDto);
  }

  @Put("/:id")
  update(@Param("id", ParseIntPipe) id: number, @Body() updateCategoryDto: UpdateCategoryDto) {
    return this.categoriesService.update(id, updateCategoryDto);
  }

  @Delete("/:id")
  delete(@Param("id", ParseIntPipe) id: number) {
    return this.categoriesService.delete(id);
  }
}

/categories.module.ts:

import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Category } from "@src/database/entities/category";
import { CategoriesController } from "./categories.controller";
import { CategoriesService } from "./categories.service";
import { CategoryMapper } from "./categories.mapper";

@Module({
  imports: [TypeOrmModule.forFeature([Category], "postgres")],
  controllers: [CategoriesController],
  providers: [CategoriesService, CategoryMapper],
  exports: [CategoriesService],
})
export class CategoriesModule {}

Test the endpoints

Create the following files inside the folder /rest-client/ and paste the content:

/products.http:

@base_url=http://localhost:3000/api/products

### Get All Products
GET {{base_url}}?pageNo=0&pageSize=10

### Get Product by ID
GET {{base_url}}/1

### Create a New Product
POST {{base_url}}
Content-Type: application/json

{
  "name": "Smartphone X",
  "description": "A high-end smartphone with an excellent camera.",
  "price": 999.99,
  "categories": [1, 2]
}

### Update Product
PUT {{base_url}}/1
Content-Type: application/json

{
  "name": "Smartphone Y",
  "description": "An updated version of Smartphone X.",
  "price": 899.99,
  "categories": [2]
}

### Delete Product
DELETE {{base_url}}/1

/categories.http:

@base_url = http://localhost:3000/api/categories

### Get All Categories
GET {{base_url}}?pageNo=0&pageSize=10

### Get Category by ID
GET {{base_url}}/1

### Create a New Category
POST {{base_url}}
Content-Type: application/json

{
  "name": "Electronics"
}

### Update Category
PUT {{base_url}}/1
Content-Type: application/json

{
  "name": "Updated Electronics"
}

### Delete Category
DELETE {{base_url}}/1

Now run the project with the following command:

npm run start

Conclusion

That’s it. Now, we have a basic REST API working in NestJS. You can download the source code on my GitHub.

Any ideas for a tutorial or suggestions to improve this project? Feel free to share them in the comments!

Thank you for reading.