Welcome back! If you missed Part 1 of my NestJS journey, I covered the basics: modules, controllers, services, DTOs, and validation.
Now we’re getting into the good stuff—the patterns that separate messy code from clean, scalable backends.
What I learned this week:
- Global exception filters
- Environment configuration with ConfigService
- The difference between @ Param, @ Query, and @ Body (Have to add space to the names because I noticed some users have them as their username)
- Building flexible filtering logic for APIs
Let’s dive in.
Day 7: Global Exception Filters - Stop Repeating Yourself
The Problem with Try-Catch Everywhere
When I first started, my controllers looked …
Welcome back! If you missed Part 1 of my NestJS journey, I covered the basics: modules, controllers, services, DTOs, and validation.
Now we’re getting into the good stuff—the patterns that separate messy code from clean, scalable backends.
What I learned this week:
- Global exception filters
- Environment configuration with ConfigService
- The difference between @ Param, @ Query, and @ Body (Have to add space to the names because I noticed some users have them as their username)
- Building flexible filtering logic for APIs
Let’s dive in.
Day 7: Global Exception Filters - Stop Repeating Yourself
The Problem with Try-Catch Everywhere
When I first started, my controllers looked like this:
@Get(':id')
async findOne(@Param('id') id: string) {
try {
return await this.productsService.findOne(id);
} catch (error) {
if (error instanceof NotFoundException) {
return { statusCode: 404, message: 'Product not found' };
}
return { statusCode: 500, message: 'Something went wrong' };
}
}
This is scattered, repetitive, and hard to maintain. Every endpoint needs the same error handling logic copy-pasted.
The Solution: Global Exception Filters
NestJS has a better way. Exception filters catch all thrown errors in one place and format responses consistently.
Here’s how I set it up:
Step 1: Create the Exception Filter
// global-filters/http-exception-filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
Logger,
} from '@nestjs/common';
import { Response } from 'express';
import { ConfigService } from '@nestjs/config';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);
constructor(private configService: ConfigService) {}
private buildErrorResponse(
status: number,
errorResponse: any,
includeStack: boolean,
stack?: string,
) {
const base = {
success: false,
statusCode: status,
error: errorResponse,
timestamp: new Date().toISOString(),
};
return includeStack ? { ...base, stackTrace: stack } : base;
}
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();
const errorResponse = exception.getResponse();
const isProduction =
this.configService.get<string>('NODE_ENV') === 'production';
this.logger.error(
`Status ${status} Error Response: ${JSON.stringify(errorResponse)}`,
);
response.status(status).json(
isProduction
? this.buildErrorResponse(status, errorResponse, false)
: this.buildErrorResponse(status, errorResponse, true, exception.stack),
);
}
}
What’s happening here?
- @Catch(HttpException) - This filter only catches HTTP exceptions
- Logger - Logs errors to console (can extend to log files/Sentry later)
- ConfigService - Reads environment variables
- Environment-based responses:
- Development: Shows full stack traces for debugging
- Production: Hides sensitive details
Step 2: Set Up Environment Variables
Install the config package:
npm i --save @nestjs/config
Create a .env file:
NODE_ENV=development
APP_PORT=3000
Update app.module.ts:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }), // Makes .env available everywhere
// ... other modules
],
})
export class AppModule {}
Step 3: Apply the Filter Globally
In main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { HttpExceptionFilter } from './global-filters/http-exception-filter';
import { ConfigService } from '@nestjs/config';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get<ConfigService>(ConfigService);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
app.useGlobalFilters(new HttpExceptionFilter(configService));
const port = configService.get<number>('APP_PORT') || 3000;
await app.listen(port);
}
bootstrap();
Now Your Controllers Are Clean AF
@Get(':id')
findOne(@Param('id') id: string) {
return this.productsService.findOne(+id);
}
That’s it. No try-catch. If the service throws a NotFoundException, the filter catches it and returns:
Development response:
{
"success": false,
"statusCode": 404,
"error": "Product not found",
"timestamp": "2025-11-11T10:30:00.000Z",
"stackTrace": "Error: Product not found\n at ProductsService.findOne..."
}
Production response:
{
"success": false,
"statusCode": 404,
"error": "Product not found",
"timestamp": "2025-11-11T10:30:00.000Z"
}
Why This Approach Wins
- Centralized error handling - One place to manage all errors
- Cleaner code - Controllers focus on business logic
- Consistent API responses - Frontend devs will love you
- Better debugging - Errors are logged and formatted properly
- Scalability - Add error tracking (Sentry, Datadog) in one place
Note: This doesn’t mean you’ll never use try-catch in NestJS. But for standard HTTP exceptions, filters handle it better.
Days 8-12: WordPress Side Quest
Real talk—I worked on my 9-5 WordPress project these days. Learning NestJS doesn’t pay the bills yet. 😅
But I kept the concepts fresh by thinking about how I’d structure the WordPress project if it were NestJS (spoiler: it would be way cleaner).
Day 13: Mastering @ Param, @ Query, and @ Body
Time to understand how to extract data from incoming requests properly.
The Three Decorators
| Decorator | Used For | Example URL |
|---|---|---|
@Body() | POST/PUT request body | N/A |
@Param() | Route parameters | /products/:id |
@Query() | Query strings | /products?name=shirt&type=clothing |
@ Body - Extracting Request Body
Used for creating or updating resources:
@Post()
create(@Body() createProductDto: CreateProductDto) {
return this.productsService.create(createProductDto);
}
Request body:
{
"productName": "Laptop",
"productType": "electronics",
"price": 50000
}
@ Param - Extracting Route Parameters
Used for identifying specific resources:
@Get(':id')
findOne(@Param('id') id: string) {
return this.productsService.findOne(+id);
}
URL: http://localhost:3000/products/5
You can extract multiple params:
@Get(':category/:id')
findByCategory(
@Param('category') category: string,
@Param('id') id: string,
) {
return this.productsService.findByCategoryAndId(category, +id);
}
URL: http://localhost:3000/products/electronics/5
@Query - Building Flexible Filters
This is where things get interesting. Query parameters are optional and perfect for filtering.
Step 1: Create a Query DTO
// dto/find-product-query.dto.ts
import { IsOptional, IsString } from 'class-validator';
export class FindProductQueryDto {
@IsOptional()
@IsString()
productName?: string;
@IsOptional()
@IsString()
productType?: string;
}
The @IsOptional() decorator means these fields don’t have to be present.
Step 2: Use It in the Controller
@Get()
findAll(@Query() query: FindProductQueryDto) {
return this.productsService.findAll(query);
}
Step 3: Implement Filtering Logic in the Service
findAll(query?: FindProductQueryDto) {
// If no query params, return all products
if (!query || Object.keys(query).length === 0) {
return {
message: 'All products fetched successfully',
data: this.products,
};
}
// Filter based on query params
const filtered = this.products.filter((prod) => {
const matchesName = query.productName
? prod.productName.toLowerCase().includes(query.productName.toLowerCase())
: true;
const matchesType = query.productType
? prod.productType.toLowerCase() === query.productType.toLowerCase()
: true;
return matchesName && matchesType; // Both conditions must match
});
return {
message: 'Filtered products',
data: filtered,
};
}
How It Works
Request: GET /products?productName=laptop&productType=electronics
Logic:
- Check if
productNamematches (case-insensitive, partial match) - Check if
productTypematches (case-insensitive, exact match) - Return products that match both conditions
Using && vs ||:
&&(AND) - Narrows the search (must match all filters)||(OR) - Broadens the search (matches any filter)
I used && because I wanted to narrow down results progressively.
When to Use What?
Use @Query for:
- Optional filtering (
/products?name=laptop&sort=asc) - Pagination (
/products?page=1&limit=10) - Search functionality
Use @ Param for:
- Required identifiers (
/products/:id) - Hierarchical resources (
/users/:userId/orders/:orderId)
Use @ Body for:
- Creating resources (POST)
- Updating resources (PUT/PATCH)
Key Takeaways from Week 2
- Global exception filters > try-catch everywhere
- ConfigService makes environment management clean
- @Query is perfect for flexible, optional filtering
- @ Param is for required identifiers in the URL
- DTOs with @IsOptional() make query validation easy
What’s Next?
Now that I’ve got the fundamentals down, it’s time to level up:
- Database integration (Mongoose + MongoDB)
- Authentication with JWT
- Guards and middleware
- Database relations (User → Products → Orders)
If you’re following along, let me know what you’d like me to cover next!
Connect With Me
I’m documenting my transition from frontend to full-stack. Let’s connect and learn together!
Drop a comment if you’re also learning NestJS or have tips for backend beginners! 👇
This is part of my #LearningInPublic series where I document my journey from frontend to full-stack development. Follow along for more!