-
Notifications
You must be signed in to change notification settings - Fork 71
Feat/implement comprehensive dto based #143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Villarley
merged 18 commits into
VolunChain:main
from
JosueBrenes:feat/Implement-Comprehensive-DTO-Based
Aug 5, 2025
Merged
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 e2e73e7
feat: add LoginDto for user authentication
JosueBrenes 43cefb0
feat: add RegisterDto for user registration
JosueBrenes 1dfb9b6
fix: add custom error message for email validation in ResendVerificatβ¦
JosueBrenes ce2db79
fix: add custom error messages for token validation in VerifyEmailDTO
JosueBrenes f871bbb
feat: add wallet validation DTOs for Stellar wallet address format
JosueBrenes 8839f82
fix: add custom validation messages for SendMessageDto and MarkAsReadDto
JosueBrenes b625903
feat: implement CreateNFTDto with validation rules for userId, organiβ¦
JosueBrenes 3e03567
feat: enhance UpdateUserDto with validation rules for name, last nameβ¦
JosueBrenes 45d1743
feat: add validation rules and messages for CreateUserDto fields inclβ¦
JosueBrenes 9aa3162
feat: refactor CreateVolunteerDTO and UpdateVolunteerDTO to use classβ¦
JosueBrenes f2c75e9
feat: replace inline validation with CreateNFTDto for NFT creation route
JosueBrenes c67d2de
feat: add authentication routes with validation middleware
JosueBrenes 22e514b
feat: add comprehensive DTO-based validation implementation guide
JosueBrenes 60d5c3d
feat: implement organization routes with validation middleware
JosueBrenes a86d80b
feat: enhance organization controller with DTOs for request validation
JosueBrenes e98e676
feat: add base DTOs for UUID parameters, pagination queries, and respβ¦
JosueBrenes aa074d7
feat: implement validation middleware with DTO validation for requestβ¦
JosueBrenes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,4 +34,7 @@ node_modules/ | |
| # Yarn Integrity file | ||
| .yarn-integrity | ||
|
|
||
| .DS_Store | ||
| .DS_Store | ||
|
|
||
| # Claude | ||
| .claude/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
Matchesfrom class-validator:import { IsString, IsEmail, MinLength, MaxLength, IsOptional, + Matches, } from "class-validator";π€ Prompt for AI Agents