All errors are caught by a global exception filter that provides consistent error responses.
Location: common/filters/http-exception.filter.ts
Response Format:
{
"statusCode": 400,
"timestamp": "2025-01-15T10:30:00.000Z",
"path": "/routine",
"method": "POST",
"message": "Validation failed",
"stack": "..." // Only in development
}Custom exceptions provide better context for specific error scenarios:
Location: common/exceptions/error-response.exception.ts
Thrown when OpenAI API calls fail.
throw new OpenAIException('Failed to generate embedding', originalError);HTTP Status: 503 Service Unavailable
Thrown when Shopify API calls fail.
throw new ShopifyException('Failed to fetch products', originalError);HTTP Status: 503 Service Unavailable
Thrown when requested products are not found.
throw new ProductNotFoundException('product-id-123');HTTP Status: 404 Not Found
All DTOs use class-validator decorators for automatic validation:
Example:
export class HairProfileDto {
@IsString()
hairColor: string;
@IsArray()
@IsString({ each: true })
hairConcerns: string[];
@IsBoolean()
recentChange: boolean;
}Validation Pipe Configuration:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Strip unknown properties
forbidNonWhitelisted: true, // Throw error on unknown properties
transform: true, // Auto-transform to DTO types
}),
);All services implement comprehensive error handling with logging:
Pattern:
async someMethod() {
try {
// Business logic
const result = await this.externalAPI.call();
if (!result) {
throw new NotFoundException('Resource not found');
}
return result;
} catch (error) {
this.logger.error('Failed to execute someMethod', error.stack);
// Re-throw known exceptions
if (error instanceof NotFoundException) {
throw error;
}
// Wrap unknown errors
throw new HttpException(
'Internal server error',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}Controllers validate input parameters and provide meaningful error messages:
Example:
@Get('products/search')
async searchProducts(
@Query('q') query: string,
@Query('limit', new DefaultValuePipe(10), new ParseIntPipe({ optional: true }))
limit?: number,
) {
if (!query || query.trim().length === 0) {
throw new BadRequestException('Query parameter "q" is required');
}
if (limit && (limit < 1 || limit > 100)) {
throw new BadRequestException('Limit must be between 1 and 100');
}
return this.catalogService.searchProductsBySimilarity(query.trim(), limit ?? 10);
}Unit tests verify individual service methods in isolation using mocks.
Location: *.spec.ts files next to source files
# Run all unit tests
npm run test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:covFile: routine/routine.service.spec.ts
Coverage:
- ✅ Successful routine generation
- ✅ Error handling (no products found)
- ✅ OpenAI API failures
- ✅ Database errors
- ✅ Private method testing
Sample Test:
it('should generate a complete hair routine successfully', async () => {
mockCatalogService.searchProductsBySimilarity.mockResolvedValue([
{ shopifyId: '123', similarity: 0.9 },
]);
mockCatalogService.getProductsByIds.mockResolvedValue(mockProducts);
jest.spyOn(service as any, 'generateStepDescription').mockResolvedValue(
'Use this product for best results',
);
const result = await service.generateRoutine(mockHairProfile);
expect(result).toBeDefined();
expect(result.message).toBe('Routine generated successfully');
expect(result.routine).toHaveLength(3);
});File: catalog/catalog.service.spec.ts
Coverage:
- ✅ Product search by similarity
- ✅ Category filtering
- ✅ Shopify API integration
- ✅ Error handling for external APIs
- ✅ Database query testing
E2E tests verify the entire application flow including HTTP requests, authentication, rate limiting, and responses.
Location: test/*.e2e-spec.ts
# Run E2E tests
npm run test:e2e
# Run specific E2E test file
npm run test:e2e -- routine.e2e-spec.tsFile: test/routine.e2e-spec.ts
Coverage:
- ✅ API key authentication
- ✅ Rate limiting enforcement
- ✅ Input validation
- ✅ Successful routine generation
- ✅ Error responses
Sample Test:
it('should return 401 without API key', () => {
return request(app.getHttpServer())
.post('/routine')
.send(validProfile)
.expect(401);
});
it('should generate routine with valid API key and profile', () => {
return request(app.getHttpServer())
.post('/routine')
.set('x-api-key', validApiKey)
.send(validProfile)
.expect(200)
.expect((res) => {
expect(res.body).toHaveProperty('message');
expect(res.body).toHaveProperty('routine');
});
});File: test/catalog.e2e-spec.ts
Coverage:
- ✅ Product search validation
- ✅ Query parameter validation
- ✅ Limit parameter bounds checking
- ✅ Category filtering
- ✅ Rate limiting for expensive operations
- ✅ Authentication requirements
| Component | Target Coverage | Current |
|---|---|---|
| Services | 80% | ✅ |
| Controllers | 70% | ✅ |
| Filters | 60% | ✅ |
| Overall | 75% | Check with npm run test:cov |
npm run test:cov
# Open coverage report in browser
open coverage/lcov-report/index.htmlRequest:
curl -X GET http://localhost:3000/catalog/products/searchResponse:
{
"statusCode": 400,
"timestamp": "2025-01-15T10:30:00.000Z",
"path": "/catalog/products/search",
"method": "GET",
"message": "Query parameter \"q\" is required"
}Request:
curl -X POST http://localhost:3000/routine -d '{}'Response:
{
"statusCode": 401,
"message": "Invalid or missing API key",
"error": "Unauthorized"
}Request:
curl -X POST http://localhost:3000/routine \
-H "x-api-key: valid-key" \
-d '{"hairColor":"brown",...}'Response:
{
"statusCode": 404,
"message": "Products not found",
"error": "Product Not Found"
}Request:
# 6th request within 1 minute
curl -X POST http://localhost:3000/routine \
-H "x-api-key: valid-key" \
-d '{...}'Response:
{
"statusCode": 429,
"message": "ThrottlerException: Too Many Requests",
"error": "Too Many Requests"
}Headers:
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640000060
Retry-After: 30
Response:
{
"statusCode": 500,
"timestamp": "2025-01-15T10:30:00.000Z",
"path": "/routine",
"method": "POST",
"message": "Failed to generate hair routine. Please try again later."
}Response (OpenAI Error):
{
"statusCode": 503,
"message": "OpenAI API Error: Failed to generate routine description",
"error": "OpenAI Service Unavailable",
"details": "Connection timeout"
}Response (Shopify Error):
{
"statusCode": 503,
"message": "Shopify API Error: Failed to fetch products",
"error": "Shopify Service Unavailable",
"details": "Rate limit exceeded"
}catch (error) {
this.logger.error('Descriptive error message', error.stack);
throw new CustomException('User-friendly message', error);
}Validate input at the controller level before processing:
if (!query || query.trim().length === 0) {
throw new BadRequestException('Query is required');
}Create specific exceptions for different error scenarios:
// Good
throw new ProductNotFoundException(productId);
// Bad
throw new Error('Product not found');Include relevant information in error messages:
// Good
this.logger.error(`Failed to fetch product ${productId}`, error.stack);
// Bad
this.logger.error('Error', error);Always test both success and failure scenarios:
it('should handle API errors gracefully', async () => {
mockService.method.mockRejectedValue(new Error('API Error'));
await expect(service.method()).rejects.toThrow();
});Before deployment, ensure:
- All unit tests pass (
npm run test) - All E2E tests pass (
npm run test:e2e) - Code coverage meets targets (
npm run test:cov) - Error handling tested for all services
- Input validation tested for all endpoints
- Authentication tested on protected endpoints
- Rate limiting tested
- External API failures handled gracefully
Increase timeout for specific tests:
it('should complete long operation', async () => {
// test code
}, 30000); // 30 second timeoutEnsure mocks are cleared between tests:
afterEach(() => {
jest.clearAllMocks();
});- Ensure
.envfile has valid credentials - Check database is running
- Verify external APIs are accessible
- Check rate limits aren't exceeded