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:
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
}
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:
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:
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.
Top comments (2)
Sincerely glad to your new post, I read it in one breath! But tell me, have you decided to give up Java?
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!