Initial commit: The Ultimate Antigravity Skills Collection (58 Skills)
This commit is contained in:
754
skills/backend-dev-guidelines/resources/validation-patterns.md
Normal file
754
skills/backend-dev-guidelines/resources/validation-patterns.md
Normal file
@@ -0,0 +1,754 @@
|
||||
# Validation Patterns - Input Validation with Zod
|
||||
|
||||
Complete guide to input validation using Zod schemas for type-safe validation.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Why Zod?](#why-zod)
|
||||
- [Basic Zod Patterns](#basic-zod-patterns)
|
||||
- [Schema Examples from Codebase](#schema-examples-from-codebase)
|
||||
- [Route-Level Validation](#route-level-validation)
|
||||
- [Controller Validation](#controller-validation)
|
||||
- [DTO Pattern](#dto-pattern)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Advanced Patterns](#advanced-patterns)
|
||||
|
||||
---
|
||||
|
||||
## Why Zod?
|
||||
|
||||
### Benefits Over Joi/Other Libraries
|
||||
|
||||
**Type Safety:**
|
||||
- ✅ Full TypeScript inference
|
||||
- ✅ Runtime + compile-time validation
|
||||
- ✅ Automatic type generation
|
||||
|
||||
**Developer Experience:**
|
||||
- ✅ Intuitive API
|
||||
- ✅ Composable schemas
|
||||
- ✅ Excellent error messages
|
||||
|
||||
**Performance:**
|
||||
- ✅ Fast validation
|
||||
- ✅ Small bundle size
|
||||
- ✅ Tree-shakeable
|
||||
|
||||
### Migration from Joi
|
||||
|
||||
Modern validation uses Zod instead of Joi:
|
||||
|
||||
```typescript
|
||||
// ❌ OLD - Joi (being phased out)
|
||||
const schema = Joi.object({
|
||||
email: Joi.string().email().required(),
|
||||
name: Joi.string().min(3).required(),
|
||||
});
|
||||
|
||||
// ✅ NEW - Zod (preferred)
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(3),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Basic Zod Patterns
|
||||
|
||||
### Primitive Types
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
// Strings
|
||||
const nameSchema = z.string();
|
||||
const emailSchema = z.string().email();
|
||||
const urlSchema = z.string().url();
|
||||
const uuidSchema = z.string().uuid();
|
||||
const minLengthSchema = z.string().min(3);
|
||||
const maxLengthSchema = z.string().max(100);
|
||||
|
||||
// Numbers
|
||||
const ageSchema = z.number().int().positive();
|
||||
const priceSchema = z.number().positive();
|
||||
const rangeSchema = z.number().min(0).max(100);
|
||||
|
||||
// Booleans
|
||||
const activeSchema = z.boolean();
|
||||
|
||||
// Dates
|
||||
const dateSchema = z.string().datetime(); // ISO 8601 string
|
||||
const nativeDateSchema = z.date(); // Native Date object
|
||||
|
||||
// Enums
|
||||
const roleSchema = z.enum(['admin', 'operations', 'user']);
|
||||
const statusSchema = z.enum(['PENDING', 'APPROVED', 'REJECTED']);
|
||||
```
|
||||
|
||||
### Objects
|
||||
|
||||
```typescript
|
||||
// Simple object
|
||||
const userSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string(),
|
||||
age: z.number().int().positive(),
|
||||
});
|
||||
|
||||
// Nested objects
|
||||
const addressSchema = z.object({
|
||||
street: z.string(),
|
||||
city: z.string(),
|
||||
zipCode: z.string().regex(/^\d{5}$/),
|
||||
});
|
||||
|
||||
const userWithAddressSchema = z.object({
|
||||
name: z.string(),
|
||||
address: addressSchema,
|
||||
});
|
||||
|
||||
// Optional fields
|
||||
const userSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email().optional(),
|
||||
phone: z.string().optional(),
|
||||
});
|
||||
|
||||
// Nullable fields
|
||||
const userSchema = z.object({
|
||||
name: z.string(),
|
||||
middleName: z.string().nullable(),
|
||||
});
|
||||
```
|
||||
|
||||
### Arrays
|
||||
|
||||
```typescript
|
||||
// Array of primitives
|
||||
const rolesSchema = z.array(z.string());
|
||||
const numbersSchema = z.array(z.number());
|
||||
|
||||
// Array of objects
|
||||
const usersSchema = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
);
|
||||
|
||||
// Array with constraints
|
||||
const tagsSchema = z.array(z.string()).min(1).max(10);
|
||||
const nonEmptyArray = z.array(z.string()).nonempty();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schema Examples from Codebase
|
||||
|
||||
### Form Validation Schemas
|
||||
|
||||
**File:** `/form/src/helpers/zodSchemas.ts`
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
// Question types enum
|
||||
export const questionTypeSchema = z.enum([
|
||||
'input',
|
||||
'textbox',
|
||||
'editor',
|
||||
'dropdown',
|
||||
'autocomplete',
|
||||
'checkbox',
|
||||
'radio',
|
||||
'upload',
|
||||
]);
|
||||
|
||||
// Upload types
|
||||
export const uploadTypeSchema = z.array(
|
||||
z.enum(['pdf', 'image', 'excel', 'video', 'powerpoint', 'word']).nullable()
|
||||
);
|
||||
|
||||
// Input types
|
||||
export const inputTypeSchema = z
|
||||
.enum(['date', 'number', 'input', 'currency'])
|
||||
.nullable();
|
||||
|
||||
// Question option
|
||||
export const questionOptionSchema = z.object({
|
||||
id: z.number().int().positive().optional(),
|
||||
controlTag: z.string().max(150).nullable().optional(),
|
||||
label: z.string().max(100).nullable().optional(),
|
||||
order: z.number().int().min(0).default(0),
|
||||
});
|
||||
|
||||
// Question schema
|
||||
export const questionSchema = z.object({
|
||||
id: z.number().int().positive().optional(),
|
||||
formID: z.number().int().positive(),
|
||||
sectionID: z.number().int().positive().optional(),
|
||||
options: z.array(questionOptionSchema).optional(),
|
||||
label: z.string().max(500),
|
||||
description: z.string().max(5000).optional(),
|
||||
type: questionTypeSchema,
|
||||
uploadTypes: uploadTypeSchema.optional(),
|
||||
inputType: inputTypeSchema.optional(),
|
||||
tags: z.array(z.string().max(150)).optional(),
|
||||
required: z.boolean(),
|
||||
isStandard: z.boolean().optional(),
|
||||
deprecatedKey: z.string().nullable().optional(),
|
||||
maxLength: z.number().int().positive().nullable().optional(),
|
||||
isOptionsSorted: z.boolean().optional(),
|
||||
});
|
||||
|
||||
// Form section schema
|
||||
export const formSectionSchema = z.object({
|
||||
id: z.number().int().positive(),
|
||||
formID: z.number().int().positive(),
|
||||
questions: z.array(questionSchema).optional(),
|
||||
label: z.string().max(500),
|
||||
description: z.string().max(5000).optional(),
|
||||
isStandard: z.boolean(),
|
||||
});
|
||||
|
||||
// Create form schema
|
||||
export const createFormSchema = z.object({
|
||||
id: z.number().int().positive(),
|
||||
label: z.string().max(150),
|
||||
description: z.string().max(6000).nullable().optional(),
|
||||
isPhase: z.boolean().optional(),
|
||||
username: z.string(),
|
||||
});
|
||||
|
||||
// Update order schema
|
||||
export const updateOrderSchema = z.object({
|
||||
source: z.object({
|
||||
index: z.number().int().min(0),
|
||||
sectionID: z.number().int().min(0),
|
||||
}),
|
||||
destination: z.object({
|
||||
index: z.number().int().min(0),
|
||||
sectionID: z.number().int().min(0),
|
||||
}),
|
||||
});
|
||||
|
||||
// Controller-specific validation schemas
|
||||
export const createQuestionValidationSchema = z.object({
|
||||
formID: z.number().int().positive(),
|
||||
sectionID: z.number().int().positive(),
|
||||
question: questionSchema,
|
||||
index: z.number().int().min(0).nullable().optional(),
|
||||
username: z.string(),
|
||||
});
|
||||
|
||||
export const updateQuestionValidationSchema = z.object({
|
||||
questionID: z.number().int().positive(),
|
||||
username: z.string(),
|
||||
question: questionSchema,
|
||||
});
|
||||
```
|
||||
|
||||
### Proxy Relationship Schema
|
||||
|
||||
```typescript
|
||||
// Proxy relationship validation
|
||||
const createProxySchema = z.object({
|
||||
originalUserID: z.string().min(1),
|
||||
proxyUserID: z.string().min(1),
|
||||
startsAt: z.string().datetime(),
|
||||
expiresAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
// With custom validation
|
||||
const createProxySchemaWithValidation = createProxySchema.refine(
|
||||
(data) => new Date(data.expiresAt) > new Date(data.startsAt),
|
||||
{
|
||||
message: 'expiresAt must be after startsAt',
|
||||
path: ['expiresAt'],
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Workflow Validation
|
||||
|
||||
```typescript
|
||||
// Workflow start schema
|
||||
const startWorkflowSchema = z.object({
|
||||
workflowCode: z.string().min(1),
|
||||
entityType: z.enum(['Post', 'User', 'Comment']),
|
||||
entityID: z.number().int().positive(),
|
||||
dryRun: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
// Workflow step completion schema
|
||||
const completeStepSchema = z.object({
|
||||
stepInstanceID: z.number().int().positive(),
|
||||
answers: z.record(z.string(), z.any()),
|
||||
dryRun: z.boolean().optional().default(false),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Route-Level Validation
|
||||
|
||||
### Pattern 1: Inline Validation
|
||||
|
||||
```typescript
|
||||
// routes/proxyRoutes.ts
|
||||
import { z } from 'zod';
|
||||
|
||||
const createProxySchema = z.object({
|
||||
originalUserID: z.string().min(1),
|
||||
proxyUserID: z.string().min(1),
|
||||
startsAt: z.string().datetime(),
|
||||
expiresAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
SSOMiddlewareClient.verifyLoginStatus,
|
||||
async (req, res) => {
|
||||
try {
|
||||
// Validate at route level
|
||||
const validated = createProxySchema.parse(req.body);
|
||||
|
||||
// Delegate to service
|
||||
const proxy = await proxyService.createProxyRelationship(validated);
|
||||
|
||||
res.status(201).json({ success: true, data: proxy });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Validation failed',
|
||||
details: error.errors,
|
||||
},
|
||||
});
|
||||
}
|
||||
handler.handleException(res, error);
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Quick and simple
|
||||
- Good for simple routes
|
||||
|
||||
**Cons:**
|
||||
- Validation logic in routes
|
||||
- Harder to test
|
||||
- Not reusable
|
||||
|
||||
---
|
||||
|
||||
## Controller Validation
|
||||
|
||||
### Pattern 2: Controller Validation (Recommended)
|
||||
|
||||
```typescript
|
||||
// validators/userSchemas.ts
|
||||
import { z } from 'zod';
|
||||
|
||||
export const createUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(2).max(100),
|
||||
roles: z.array(z.enum(['admin', 'operations', 'user'])),
|
||||
isActive: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const updateUserSchema = z.object({
|
||||
email: z.string().email().optional(),
|
||||
name: z.string().min(2).max(100).optional(),
|
||||
roles: z.array(z.enum(['admin', 'operations', 'user'])).optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type CreateUserDTO = z.infer<typeof createUserSchema>;
|
||||
export type UpdateUserDTO = z.infer<typeof updateUserSchema>;
|
||||
```
|
||||
|
||||
```typescript
|
||||
// controllers/UserController.ts
|
||||
import { Request, Response } from 'express';
|
||||
import { BaseController } from './BaseController';
|
||||
import { UserService } from '../services/userService';
|
||||
import { createUserSchema, updateUserSchema } from '../validators/userSchemas';
|
||||
import { z } from 'zod';
|
||||
|
||||
export class UserController extends BaseController {
|
||||
private userService: UserService;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.userService = new UserService();
|
||||
}
|
||||
|
||||
async createUser(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
// Validate input
|
||||
const validated = createUserSchema.parse(req.body);
|
||||
|
||||
// Call service
|
||||
const user = await this.userService.createUser(validated);
|
||||
|
||||
this.handleSuccess(res, user, 'User created successfully', 201);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
// Handle validation errors with 400 status
|
||||
return this.handleError(error, res, 'createUser', 400);
|
||||
}
|
||||
this.handleError(error, res, 'createUser');
|
||||
}
|
||||
}
|
||||
|
||||
async updateUser(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
// Validate params and body
|
||||
const userId = req.params.id;
|
||||
const validated = updateUserSchema.parse(req.body);
|
||||
|
||||
const user = await this.userService.updateUser(userId, validated);
|
||||
|
||||
this.handleSuccess(res, user, 'User updated successfully');
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return this.handleError(error, res, 'updateUser', 400);
|
||||
}
|
||||
this.handleError(error, res, 'updateUser');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Clean separation
|
||||
- Reusable schemas
|
||||
- Easy to test
|
||||
- Type-safe DTOs
|
||||
|
||||
**Cons:**
|
||||
- More files to manage
|
||||
|
||||
---
|
||||
|
||||
## DTO Pattern
|
||||
|
||||
### Type Inference from Schemas
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
// Define schema
|
||||
const createUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string(),
|
||||
age: z.number().int().positive(),
|
||||
});
|
||||
|
||||
// Infer TypeScript type from schema
|
||||
type CreateUserDTO = z.infer<typeof createUserSchema>;
|
||||
|
||||
// Equivalent to:
|
||||
// type CreateUserDTO = {
|
||||
// email: string;
|
||||
// name: string;
|
||||
// age: number;
|
||||
// }
|
||||
|
||||
// Use in service
|
||||
class UserService {
|
||||
async createUser(data: CreateUserDTO): Promise<User> {
|
||||
// data is fully typed!
|
||||
console.log(data.email); // ✅ TypeScript knows this exists
|
||||
console.log(data.invalid); // ❌ TypeScript error!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Input vs Output Types
|
||||
|
||||
```typescript
|
||||
// Input schema (what API receives)
|
||||
const createUserInputSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string(),
|
||||
password: z.string().min(8),
|
||||
});
|
||||
|
||||
// Output schema (what API returns)
|
||||
const userOutputSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
email: z.string().email(),
|
||||
name: z.string(),
|
||||
createdAt: z.string().datetime(),
|
||||
// password excluded!
|
||||
});
|
||||
|
||||
type CreateUserInput = z.infer<typeof createUserInputSchema>;
|
||||
type UserOutput = z.infer<typeof userOutputSchema>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Zod Error Format
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const validated = schema.parse(data);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
console.log(error.errors);
|
||||
// [
|
||||
// {
|
||||
// code: 'invalid_type',
|
||||
// expected: 'string',
|
||||
// received: 'number',
|
||||
// path: ['email'],
|
||||
// message: 'Expected string, received number'
|
||||
// }
|
||||
// ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Error Messages
|
||||
|
||||
```typescript
|
||||
const userSchema = z.object({
|
||||
email: z.string().email({ message: 'Please provide a valid email address' }),
|
||||
name: z.string().min(2, { message: 'Name must be at least 2 characters' }),
|
||||
age: z.number().int().positive({ message: 'Age must be a positive number' }),
|
||||
});
|
||||
```
|
||||
|
||||
### Formatted Error Response
|
||||
|
||||
```typescript
|
||||
// Helper function to format Zod errors
|
||||
function formatZodError(error: z.ZodError) {
|
||||
return {
|
||||
message: 'Validation failed',
|
||||
errors: error.errors.map((err) => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
code: err.code,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// In controller
|
||||
catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: formatZodError(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Response example:
|
||||
// {
|
||||
// "success": false,
|
||||
// "error": {
|
||||
// "message": "Validation failed",
|
||||
// "errors": [
|
||||
// {
|
||||
// "field": "email",
|
||||
// "message": "Invalid email",
|
||||
// "code": "invalid_string"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Conditional Validation
|
||||
|
||||
```typescript
|
||||
// Validate based on other field values
|
||||
const submissionSchema = z.object({
|
||||
type: z.enum(['NEW', 'UPDATE']),
|
||||
postId: z.number().optional(),
|
||||
}).refine(
|
||||
(data) => {
|
||||
// If type is UPDATE, postId is required
|
||||
if (data.type === 'UPDATE') {
|
||||
return data.postId !== undefined;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'postId is required when type is UPDATE',
|
||||
path: ['postId'],
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Transform Data
|
||||
|
||||
```typescript
|
||||
// Transform strings to numbers
|
||||
const userSchema = z.object({
|
||||
name: z.string(),
|
||||
age: z.string().transform((val) => parseInt(val, 10)),
|
||||
});
|
||||
|
||||
// Transform dates
|
||||
const eventSchema = z.object({
|
||||
name: z.string(),
|
||||
date: z.string().transform((str) => new Date(str)),
|
||||
});
|
||||
```
|
||||
|
||||
### Preprocess Data
|
||||
|
||||
```typescript
|
||||
// Trim strings before validation
|
||||
const userSchema = z.object({
|
||||
email: z.preprocess(
|
||||
(val) => typeof val === 'string' ? val.trim().toLowerCase() : val,
|
||||
z.string().email()
|
||||
),
|
||||
name: z.preprocess(
|
||||
(val) => typeof val === 'string' ? val.trim() : val,
|
||||
z.string().min(2)
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
### Union Types
|
||||
|
||||
```typescript
|
||||
// Multiple possible types
|
||||
const idSchema = z.union([z.string(), z.number()]);
|
||||
|
||||
// Discriminated unions
|
||||
const notificationSchema = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('email'),
|
||||
recipient: z.string().email(),
|
||||
subject: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('sms'),
|
||||
phoneNumber: z.string(),
|
||||
message: z.string(),
|
||||
}),
|
||||
]);
|
||||
```
|
||||
|
||||
### Recursive Schemas
|
||||
|
||||
```typescript
|
||||
// For nested structures like trees
|
||||
type Category = {
|
||||
id: number;
|
||||
name: string;
|
||||
children?: Category[];
|
||||
};
|
||||
|
||||
const categorySchema: z.ZodType<Category> = z.lazy(() =>
|
||||
z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
children: z.array(categorySchema).optional(),
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
### Schema Composition
|
||||
|
||||
```typescript
|
||||
// Base schemas
|
||||
const timestampsSchema = z.object({
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
const auditSchema = z.object({
|
||||
createdBy: z.string(),
|
||||
updatedBy: z.string(),
|
||||
});
|
||||
|
||||
// Compose schemas
|
||||
const userSchema = z.object({
|
||||
id: z.string(),
|
||||
email: z.string().email(),
|
||||
name: z.string(),
|
||||
}).merge(timestampsSchema).merge(auditSchema);
|
||||
|
||||
// Extend schemas
|
||||
const adminUserSchema = userSchema.extend({
|
||||
adminLevel: z.number().int().min(1).max(5),
|
||||
permissions: z.array(z.string()),
|
||||
});
|
||||
|
||||
// Pick specific fields
|
||||
const publicUserSchema = userSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
// email excluded
|
||||
});
|
||||
|
||||
// Omit fields
|
||||
const userWithoutTimestamps = userSchema.omit({
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
```
|
||||
|
||||
### Validation Middleware
|
||||
|
||||
```typescript
|
||||
// Create reusable validation middleware
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { z } from 'zod';
|
||||
|
||||
export function validateBody<T extends z.ZodType>(schema: T) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
req.body = schema.parse(req.body);
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Validation failed',
|
||||
details: error.errors,
|
||||
},
|
||||
});
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
router.post('/users',
|
||||
validateBody(createUserSchema),
|
||||
async (req, res) => {
|
||||
// req.body is validated and typed!
|
||||
const user = await userService.createUser(req.body);
|
||||
res.json({ success: true, data: user });
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Related Files:**
|
||||
- [SKILL.md](SKILL.md) - Main guide
|
||||
- [routing-and-controllers.md](routing-and-controllers.md) - Using validation in controllers
|
||||
- [services-and-repositories.md](services-and-repositories.md) - Using DTOs in services
|
||||
- [async-and-errors.md](async-and-errors.md) - Error handling patterns
|
||||
Reference in New Issue
Block a user