New skills (14): - nestjs-best-practices: 40 priority-ranked rules (kadajett) - fastapi: Pydantic v2, async SQLAlchemy, JWT auth (jezweb) - architecture-patterns: Clean Architecture, Hexagonal, DDD (wshobson) - python-performance-optimization: Profiling and optimization (wshobson) - ai-sdk: Vercel AI SDK streaming and agent patterns (vercel) - create-agent: Modular agent architecture with OpenRouter (openrouterteam) - proactive-agent: WAL Protocol, compaction recovery, self-improvement (halthelobster) - brand-guidelines: Brand identity enforcement (anthropics) - ui-animation: Motion design with accessibility (mblode) - marketing-ideas: 139 ideas across 14 categories (coreyhaines31) - pricing-strategy: SaaS pricing and tier design (coreyhaines31) - programmatic-seo: SEO at scale with playbooks (coreyhaines31) - competitor-alternatives: Comparison page architecture (coreyhaines31) - referral-program: Referral and affiliate programs (coreyhaines31) README reorganized by domain: Code Quality, Frontend, Backend, Auth, AI/Agent Building, Marketing, Design, Meta. Mosaic Stack is not limited to coding — the Orchestrator serves coding, business, design, marketing, writing, logistics, and analysis. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
115 lines
3.1 KiB
Markdown
115 lines
3.1 KiB
Markdown
---
|
|
title: Throw HTTP Exceptions from Services
|
|
impact: HIGH
|
|
impactDescription: Keeps controllers thin and simplifies error handling
|
|
tags: error-handling, exceptions, services
|
|
---
|
|
|
|
## Throw HTTP Exceptions from Services
|
|
|
|
It's acceptable (and often preferable) to throw `HttpException` subclasses from services in HTTP applications. This keeps controllers thin and allows services to communicate appropriate error states. For truly layer-agnostic services, use domain exceptions that map to HTTP status codes.
|
|
|
|
**Incorrect (return error objects instead of throwing):**
|
|
|
|
```typescript
|
|
// Return error objects instead of throwing
|
|
@Injectable()
|
|
export class UsersService {
|
|
async findById(id: string): Promise<{ user?: User; error?: string }> {
|
|
const user = await this.repo.findOne({ where: { id } });
|
|
if (!user) {
|
|
return { error: 'User not found' }; // Controller must check this
|
|
}
|
|
return { user };
|
|
}
|
|
}
|
|
|
|
@Controller('users')
|
|
export class UsersController {
|
|
@Get(':id')
|
|
async findOne(@Param('id') id: string) {
|
|
const result = await this.usersService.findById(id);
|
|
if (result.error) {
|
|
throw new NotFoundException(result.error);
|
|
}
|
|
return result.user;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Correct (throw exceptions directly from service):**
|
|
|
|
```typescript
|
|
// Throw exceptions directly from service
|
|
@Injectable()
|
|
export class UsersService {
|
|
constructor(private readonly repo: UserRepository) {}
|
|
|
|
async findById(id: string): Promise<User> {
|
|
const user = await this.repo.findOne({ where: { id } });
|
|
if (!user) {
|
|
throw new NotFoundException(`User #${id} not found`);
|
|
}
|
|
return user;
|
|
}
|
|
|
|
async create(dto: CreateUserDto): Promise<User> {
|
|
const existing = await this.repo.findOne({
|
|
where: { email: dto.email },
|
|
});
|
|
if (existing) {
|
|
throw new ConflictException('Email already registered');
|
|
}
|
|
return this.repo.save(dto);
|
|
}
|
|
|
|
async update(id: string, dto: UpdateUserDto): Promise<User> {
|
|
const user = await this.findById(id); // Throws if not found
|
|
Object.assign(user, dto);
|
|
return this.repo.save(user);
|
|
}
|
|
}
|
|
|
|
// Controller stays thin
|
|
@Controller('users')
|
|
export class UsersController {
|
|
@Get(':id')
|
|
findOne(@Param('id') id: string): Promise<User> {
|
|
return this.usersService.findById(id);
|
|
}
|
|
|
|
@Post()
|
|
create(@Body() dto: CreateUserDto): Promise<User> {
|
|
return this.usersService.create(dto);
|
|
}
|
|
}
|
|
|
|
// For layer-agnostic services, use domain exceptions
|
|
export class EntityNotFoundException extends Error {
|
|
constructor(
|
|
public readonly entity: string,
|
|
public readonly id: string,
|
|
) {
|
|
super(`${entity} with ID "${id}" not found`);
|
|
}
|
|
}
|
|
|
|
// Map to HTTP in exception filter
|
|
@Catch(EntityNotFoundException)
|
|
export class EntityNotFoundFilter implements ExceptionFilter {
|
|
catch(exception: EntityNotFoundException, host: ArgumentsHost) {
|
|
const ctx = host.switchToHttp();
|
|
const response = ctx.getResponse<Response>();
|
|
|
|
response.status(404).json({
|
|
statusCode: 404,
|
|
message: exception.message,
|
|
entity: exception.entity,
|
|
id: exception.id,
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
Reference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters)
|