DEV Community

Tochukwu Nwosa
Tochukwu Nwosa

Posted on

NestJS Week 2: Exception Filters, Query Params, and Why You Should Stop Using Try-Catch Everywhere

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

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

What's happening here?

  1. @Catch(HttpException) - This filter only catches HTTP exceptions
  2. Logger - Logs errors to console (can extend to log files/Sentry later)
  3. ConfigService - Reads environment variables
  4. 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
Enter fullscreen mode Exit fullscreen mode

Create a .env file:

NODE_ENV=development
APP_PORT=3000
Enter fullscreen mode Exit fullscreen mode

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

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

Now Your Controllers Are Clean AF

@Get(':id')
findOne(@Param('id') id: string) {
  return this.productsService.findOne(+id);
}
Enter fullscreen mode Exit fullscreen mode

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

Production response:

{
  "success": false,
  "statusCode": 404,
  "error": "Product not found",
  "timestamp": "2025-11-11T10:30:00.000Z"
}
Enter fullscreen mode Exit fullscreen mode

Why This Approach Wins

  1. Centralized error handling - One place to manage all errors
  2. Cleaner code - Controllers focus on business logic
  3. Consistent API responses - Frontend devs will love you
  4. Better debugging - Errors are logged and formatted properly
  5. 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);
}
Enter fullscreen mode Exit fullscreen mode

Request body:

{
  "productName": "Laptop",
  "productType": "electronics",
  "price": 50000
}
Enter fullscreen mode Exit fullscreen mode

@ Param - Extracting Route Parameters

Used for identifying specific resources:

@Get(':id')
findOne(@Param('id') id: string) {
  return this.productsService.findOne(+id);
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

How It Works

Request: GET /products?productName=laptop&productType=electronics

Logic:

  1. Check if productName matches (case-insensitive, partial match)
  2. Check if productType matches (case-insensitive, exact match)
  3. 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

  1. Global exception filters > try-catch everywhere
  2. ConfigService makes environment management clean
  3. @Query is perfect for flexible, optional filtering
  4. @ Param is for required identifiers in the URL
  5. 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!

🌐 Portfolio: tochukwu-nwosa.vercel.app

πŸš€ Latest Project: Tech Linkup - Discover tech events in Nigerian cities

πŸ’Ό LinkedIn: nwosa-tochukwu

πŸ™ GitHub: @tochukwunwosa

🐦 Twitter/X: @tochukwudev

πŸ“ Dev.to: @tochukwu_dev

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!

Top comments (0)