DEV Community

Cover image for Basic REST API with NestJS 2025
Javier Vega
Javier Vega

Posted on • Edited on

Basic REST API with NestJS 2025

Basic REST API with NestJS 2025

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:

Method Endpoint Description
GET /api/products Get paginated list of products
GET /api/products/{id} Get a specific product by ID
POST /api/products Create a new product
PUT /api/products/{id} Update an existing product
DELETE /api/products/{id} Delete a product

Categories:

Method Endpoint Description
GET /api/categories Get paginated list of categories
GET /api/categories/{id} Get a specific category by ID
POST /api/categories Create 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
}
Enter fullscreen mode Exit fullscreen mode

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"
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

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"
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

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"
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

DELETE /api/products/{id}

{
  "message": "Product deleted successfully"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then run this command in the root of the project:

docker compose -f docker-compose.local-dev.yaml up -d
Enter fullscreen mode Exit fullscreen mode

Start coding

Project setup

Install NestJS:

npm i -g @nestjs/cli
Enter fullscreen mode Exit fullscreen mode

Create the project with this command:

nest new products-api
Enter fullscreen mode Exit fullscreen mode

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

Which package manager would you ❤️ to use? npm
Enter fullscreen mode Exit fullscreen mode

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

Now we can install the dependencies:

npm ci
Enter fullscreen mode Exit fullscreen mode

And run the project:

npm run start
Enter fullscreen mode Exit fullscreen mode

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}}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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;
}

Enter fullscreen mode Exit fullscreen mode

/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[];
}

Enter fullscreen mode Exit fullscreen mode

/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[];
}

Enter fullscreen mode Exit fullscreen mode

/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,
    };
  }
}

Enter fullscreen mode Exit fullscreen mode

/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 {}

Enter fullscreen mode Exit fullscreen mode

Core

Delete files:

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

Install the following packages:

npm install helmet nestjs-pino @nestjs/throttler
Enter fullscreen mode Exit fullscreen mode
npm install pino-pretty --save-dev
Enter fullscreen mode Exit fullscreen mode
  • 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();
Enter fullscreen mode Exit fullscreen mode

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 {}

Enter fullscreen mode Exit fullscreen mode

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 {}

Enter fullscreen mode Exit fullscreen mode

Products

Install the following packages:

npm install class-validator class-transformer
Enter fullscreen mode Exit fullscreen mode
  • 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[];
}

Enter fullscreen mode Exit fullscreen mode

/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[];
}

Enter fullscreen mode Exit fullscreen mode

/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;
}
Enter fullscreen mode Exit fullscreen mode

/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,
        })) || [],
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

/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',
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

/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);
  }
}

Enter fullscreen mode Exit fullscreen mode

/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 {}

Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

/dtos/update-category.dto.ts:

import { IsString } from 'class-validator';

export class UpdateCategoryDto {
  @IsString()
  name: string;
}
Enter fullscreen mode Exit fullscreen mode

/dtos/category-response.dto.ts:

export class CategoryResponseDto {
  id: number;
  name: string;
}
Enter fullscreen mode Exit fullscreen mode

/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,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

/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',
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

/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);
  }
}
Enter fullscreen mode Exit fullscreen mode

/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 {}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

/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

Enter fullscreen mode Exit fullscreen mode

Now run the project with the following command:

npm run start
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
 
nikamilon_cf66fd726852666 profile image
NikaMilon

Sincerely glad to your new post, I read it in one breath! But tell me, have you decided to give up Java?

Collapse
 
javiervmc profile image
Javier Vega

Glad you enjoyed the post! I still work with Java, but lately, I've been focusing more on NestJS since I use it in my current job. I'm also working on more tutorials for both Java with Spring Boot and NestJS, as those are the two backend frameworks I've used in my jobs. Stay tuned for more content!