Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
307e8ab
fix: ensure .DS_Store and .claude/ are ignored in the project
JosueBrenes Aug 5, 2025
e2e73e7
feat: add LoginDto for user authentication
JosueBrenes Aug 5, 2025
43cefb0
feat: add RegisterDto for user registration
JosueBrenes Aug 5, 2025
1dfb9b6
fix: add custom error message for email validation in ResendVerificat…
JosueBrenes Aug 5, 2025
ce2db79
fix: add custom error messages for token validation in VerifyEmailDTO
JosueBrenes Aug 5, 2025
f871bbb
feat: add wallet validation DTOs for Stellar wallet address format
JosueBrenes Aug 5, 2025
8839f82
fix: add custom validation messages for SendMessageDto and MarkAsReadDto
JosueBrenes Aug 5, 2025
b625903
feat: implement CreateNFTDto with validation rules for userId, organi…
JosueBrenes Aug 5, 2025
3e03567
feat: enhance UpdateUserDto with validation rules for name, last name…
JosueBrenes Aug 5, 2025
45d1743
feat: add validation rules and messages for CreateUserDto fields incl…
JosueBrenes Aug 5, 2025
9aa3162
feat: refactor CreateVolunteerDTO and UpdateVolunteerDTO to use class…
JosueBrenes Aug 5, 2025
f2c75e9
feat: replace inline validation with CreateNFTDto for NFT creation route
JosueBrenes Aug 5, 2025
c67d2de
feat: add authentication routes with validation middleware
JosueBrenes Aug 5, 2025
22e514b
feat: add comprehensive DTO-based validation implementation guide
JosueBrenes Aug 5, 2025
60d5c3d
feat: implement organization routes with validation middleware
JosueBrenes Aug 5, 2025
a86d80b
feat: enhance organization controller with DTOs for request validation
JosueBrenes Aug 5, 2025
e98e676
feat: add base DTOs for UUID parameters, pagination queries, and resp…
JosueBrenes Aug 5, 2025
aa074d7
feat: implement validation middleware with DTO validation for request…
JosueBrenes Aug 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,7 @@ node_modules/
# Yarn Integrity file
.yarn-integrity

.DS_Store
.DS_Store

# Claude
.claude/
256 changes: 256 additions & 0 deletions docs/dto-validation-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
# DTO-Based Validation Implementation Guide

## Overview

This guide documents the comprehensive DTO-based validation system implemented using `class-validator` and `class-transformer`. This system replaces the previous express-validator approach with a more robust, type-safe validation mechanism that aligns with our Domain-Driven Design (DDD) architecture.

## Architecture

### Core Components

1. **Validation Middleware** (`src/shared/middleware/validation.middleware.ts`)

- `validateDto<T>()` - Validates request body
- `validateQueryDto<T>()` - Validates query parameters
- `validateParamsDto<T>()` - Validates route parameters

2. **Base DTOs** (`src/shared/dto/base.dto.ts`)

- `UuidParamsDto` - For UUID route parameters
- `PaginationQueryDto` - For pagination query parameters
- `BaseResponseDto` - Base response structure
- `ErrorResponseDto` - Error response structure

3. **Module-Specific DTOs**
- Auth: `RegisterDto`, `LoginDto`, `VerifyEmailDTO`, `ResendVerificationDTO`
- Organization: `CreateOrganizationDto`, `UpdateOrganizationDto`
- User: `CreateUserDto`, `UpdateUserDto`
- Project: `CreateProjectDto`, `UpdateProjectDto`
- NFT: `CreateNFTDto`
- Messaging: `SendMessageDto`, `MarkAsReadDto`
- Volunteer: `CreateVolunteerDTO`, `UpdateVolunteerDTO`

## Usage Examples

### 1. Route-Level Validation

```typescript
import { Router } from "express";
import {
validateDto,
validateParamsDto,
validateQueryDto,
} from "../shared/middleware/validation.middleware";
import { CreateOrganizationDto } from "../modules/organization/presentation/dto/create-organization.dto";
import { UuidParamsDto, PaginationQueryDto } from "../shared/dto/base.dto";

const router = Router();

// POST with body validation
router.post(
"/organizations",
validateDto(CreateOrganizationDto),
organizationController.create
);

// GET with parameter validation
router.get(
"/organizations/:id",
validateParamsDto(UuidParamsDto),
organizationController.getById
);

// GET with query validation
router.get(
"/organizations",
validateQueryDto(PaginationQueryDto),
organizationController.getAll
);
```

### 2. Controller Type Safety

```typescript
import { Request, Response } from "express";
import { CreateOrganizationDto } from "../dto/create-organization.dto";
import { UuidParamsDto, PaginationQueryDto } from "../../shared/dto/base.dto";

export class OrganizationController {
createOrganization = async (
req: Request<{}, {}, CreateOrganizationDto>,
res: Response
): Promise<void> => {
// req.body is now typed as CreateOrganizationDto
const organization = await this.createUseCase.execute(req.body);
res.status(201).json({ success: true, data: organization });
};

getById = async (
req: Request<UuidParamsDto>,
res: Response
): Promise<void> => {
// req.params.id is validated as UUID
const organization = await this.getUseCase.execute(req.params.id);
res.json({ success: true, data: organization });
};

getAll = async (
req: Request<{}, {}, {}, PaginationQueryDto>,
res: Response
): Promise<void> => {
// req.query is typed and validated
const { page, limit, search } = req.query;
const organizations = await this.getAllUseCase.execute({
page,
limit,
search,
});
res.json({ success: true, data: organizations });
};
}
```

### 3. Creating Custom DTOs

```typescript
import {
IsString,
IsEmail,
MinLength,
MaxLength,
IsOptional,
IsUUID,
} from "class-validator";

export class CreateOrganizationDto {
@IsString({ message: "Name must be a string" })
@MinLength(2, { message: "Name must be at least 2 characters long" })
@MaxLength(100, { message: "Name cannot exceed 100 characters" })
name: string;

@IsEmail({}, { message: "Please provide a valid email address" })
email: string;

@IsString({ message: "Password must be a string" })
@MinLength(8, { message: "Password must be at least 8 characters long" })
password: string;

@IsOptional()
@IsString({ message: "Description must be a string" })
@MinLength(10, { message: "Description must be at least 10 characters long" })
@MaxLength(500, { message: "Description cannot exceed 500 characters" })
description?: string;
}
```

## Validation Rules

### Common Validation Decorators

- `@IsString()` - Validates string type
- `@IsEmail()` - Validates email format
- `@IsUUID(4)` - Validates UUID v4 format
- `@IsInt()`, `@IsNumber()` - Validates numeric types
- `@IsBoolean()` - Validates boolean type
- `@IsOptional()` - Makes field optional
- `@MinLength(n)`, `@MaxLength(n)` - String length validation
- `@Min(n)`, `@Max(n)` - Numeric range validation
- `@Matches(regex)` - Regular expression validation
- `@IsEnum(enum)` - Enum validation
- `@IsUrl()` - URL validation

### Transform Decorators

```typescript
import { Transform } from "class-transformer";

export class PaginationQueryDto {
@Transform(({ value }) => parseInt(value, 10))
@IsInt({ message: "Page must be an integer" })
@Min(1, { message: "Page must be at least 1" })
page: number;
}
```

## Error Response Format

When validation fails, the middleware returns a standardized error response:

```json
{
"success": false,
"error": "Validation failed",
"details": [
{
"property": "email",
"value": "invalid-email",
"constraints": ["Please provide a valid email address"]
},
{
"property": "name",
"value": "A",
"constraints": ["Name must be at least 2 characters long"]
}
]
}
```

## Migration from express-validator

### Before (express-validator)

```typescript
import { body, param, validationResult } from "express-validator";

router.post(
"/nfts",
[
body("userId").isUUID(),
body("organizationId").isUUID(),
body("description").isString().notEmpty(),
],
NFTController.createNFT
);
```

### After (class-validator)

```typescript
import { validateDto } from "../shared/middleware/validation.middleware";
import { CreateNFTDto } from "../modules/nft/dto/create-nft.dto";

router.post("/nfts", validateDto(CreateNFTDto), NFTController.createNFT);
```

## Benefits

1. **Type Safety** - Full TypeScript support with typed request objects
2. **Centralized Validation** - All validation rules defined in DTO classes
3. **Reusability** - DTOs can be reused across different endpoints
4. **Consistency** - Standardized error responses
5. **Maintainability** - Easy to update validation rules in one place
6. **DDD Alignment** - Fits well with Domain-Driven Design principles
7. **Auto-transformation** - Automatic type conversion with class-transformer

## Testing

Test files are available in `src/shared/middleware/__tests__/validation.middleware.test.ts` demonstrating proper testing of validation middleware.

## Migration Checklist

- [ ] Replace express-validator imports with class-validator DTOs
- [ ] Update route handlers to use validation middleware
- [ ] Update controller method signatures with proper typing
- [ ] Test all endpoints with both valid and invalid data
- [ ] Update API documentation with new validation rules
- [ ] Remove unused express-validator dependencies (optional)

## Best Practices

1. Always provide descriptive error messages in validation decorators
2. Use appropriate validation decorators for each field type
3. Group related validations in the same DTO class
4. Use base DTOs for common patterns (UUID params, pagination)
5. Keep DTOs focused and specific to their use case
6. Test validation logic thoroughly
7. Document custom validation rules
10 changes: 10 additions & 0 deletions src/modules/auth/dto/login.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IsString, IsEmail, IsNotEmpty } from "class-validator";

export class LoginDto {
@IsEmail({}, { message: "Please provide a valid email address" })
email: string;

@IsString({ message: "Password must be a string" })
@IsNotEmpty({ message: "Password is required" })
password: string;
}
32 changes: 32 additions & 0 deletions src/modules/auth/dto/register.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
IsString,
IsEmail,
MinLength,
MaxLength,
IsOptional,
} from "class-validator";

export class RegisterDto {
@IsString({ message: "Name must be a string" })
@MinLength(2, { message: "Name must be at least 2 characters long" })
@MaxLength(100, { message: "Name cannot exceed 100 characters" })
name: string;

@IsEmail({}, { message: "Please provide a valid email address" })
email: string;

@IsString({ message: "Password must be a string" })
@MinLength(8, { message: "Password must be at least 8 characters long" })
@MaxLength(128, { message: "Password cannot exceed 128 characters" })
password: string;

@IsOptional()
@IsString({ message: "Wallet address must be a string" })
@MinLength(56, {
message: "Stellar wallet address must be 56 characters long",
})
@MaxLength(56, {
message: "Stellar wallet address must be 56 characters long",
})
walletAddress?: string;
}
Comment on lines +23 to +32
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Add regex pattern validation for Stellar wallet addresses.

The wallet address validation only checks length but missing format validation. Based on other wallet DTOs in the codebase, Stellar addresses should match the pattern /^G[A-Z2-7]{55}$/.

 @IsOptional()
 @IsString({ message: "Wallet address must be a string" })
 @MinLength(56, {
   message: "Stellar wallet address must be 56 characters long",
 })
 @MaxLength(56, {
   message: "Stellar wallet address must be 56 characters long",
 })
+@Matches(/^G[A-Z2-7]{55}$/, {
+  message: "Invalid Stellar wallet address format",
+})
 walletAddress?: string;

You'll also need to import Matches from class-validator:

 import {
   IsString,
   IsEmail,
   MinLength,
   MaxLength,
   IsOptional,
+  Matches,
 } from "class-validator";
πŸ€– Prompt for AI Agents
In src/modules/auth/dto/register.dto.ts around lines 23 to 32, the walletAddress
field currently validates only the length but lacks format validation. To fix
this, import the Matches decorator from class-validator and add a
@Matches(/^G[A-Z2-7]{55}$/, { message: "Invalid Stellar wallet address format"
}) decorator to walletAddress. This will enforce the correct Stellar address
pattern as per the codebase standards.

2 changes: 1 addition & 1 deletion src/modules/auth/dto/resendVerificationDTO.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IsEmail } from "class-validator";

export class ResendVerificationDTO {
@IsEmail()
@IsEmail({}, { message: "Please provide a valid email address" })
email: string;
}
4 changes: 2 additions & 2 deletions src/modules/auth/dto/verifyEmailDTO.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IsString, IsNotEmpty } from "class-validator";

export class VerifyEmailDTO {
@IsString()
@IsNotEmpty()
@IsString({ message: "Token must be a string" })
@IsNotEmpty({ message: "Token is required" })
token: string;
}
35 changes: 35 additions & 0 deletions src/modules/auth/dto/wallet-validation.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { IsString, MinLength, MaxLength, Matches } from "class-validator";

export class ValidateWalletFormatDto {
@IsString({ message: "Wallet address must be a string" })
@MinLength(56, {
message: "Stellar wallet address must be 56 characters long",
})
@MaxLength(56, {
message: "Stellar wallet address must be 56 characters long",
})
@Matches(/^G[A-Z2-7]{55}$/, {
message: "Invalid Stellar wallet address format",
})
walletAddress: string;
}

export class VerifyWalletDto {
@IsString({ message: "Wallet address must be a string" })
@MinLength(56, {
message: "Stellar wallet address must be 56 characters long",
})
@MaxLength(56, {
message: "Stellar wallet address must be 56 characters long",
})
@Matches(/^G[A-Z2-7]{55}$/, {
message: "Invalid Stellar wallet address format",
})
walletAddress: string;

@IsString({ message: "Signature must be a string" })
signature: string;

@IsString({ message: "Message must be a string" })
message: string;
}
16 changes: 8 additions & 8 deletions src/modules/messaging/dto/message.dto.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { IsString, IsNotEmpty, IsUUID } from "class-validator";

export class SendMessageDto {
@IsString()
@IsNotEmpty()
@IsString({ message: "Content must be a string" })
@IsNotEmpty({ message: "Content is required" })
content: string;

@IsUUID()
@IsNotEmpty()
@IsUUID(4, { message: "Receiver ID must be a valid UUID" })
@IsNotEmpty({ message: "Receiver ID is required" })
receiverId: string;

@IsUUID()
@IsNotEmpty()
@IsUUID(4, { message: "Volunteer ID must be a valid UUID" })
@IsNotEmpty({ message: "Volunteer ID is required" })
volunteerId: string;
}

export class MarkAsReadDto {
@IsUUID()
@IsNotEmpty()
@IsUUID(4, { message: "Message ID must be a valid UUID" })
@IsNotEmpty({ message: "Message ID is required" })
messageId: string;
}

Expand Down
Loading
Loading