api-validators
This skill teaches the agent how to implement request validation in the API project. MANDATORY - You MUST read this skill before modifying any validator files.
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: api-validators
description: This skill teaches the agent how to implement request validation in the API project. MANDATORY - You MUST read this skill before modifying any validator files.
---
# API Validators Skill
## Purpose
This skill covers the validation layer of the API project - using schemas to validate and transform HTTP request data before it reaches the controller.
## When to Use This Skill
- Adding validation for new endpoints
- Defining request schemas with Zod
- Validating query parameters, path params, and request bodies
- Creating reusable validation schemas
- Transforming and parsing request data
## Key Patterns from Codebase
### 1. Basic Validator Structure
```typescript
// api/src/api/validators/community.validator.ts
import { z } from 'zod';
import { Request, Response, NextFunction } from 'express';
// Define Zod schemas for each endpoint
export const createCommunitySchema = z.object({
body: z.object({
name: z.string().min(1).max(100),
description: z.string().optional(),
// Trust System Configuration
minTrustToAwardTrust: trustRequirementSchema.optional(),
// Wealth Access Configuration
minTrustForWealth: trustRequirementSchema.optional(),
}),
}).passthrough(); // Allow extra fields
export const getCommunitySchema = z.object({
params: z.object({
id: z.string().uuid(),
}),
}).passthrough();
export const updateCommunitySchema = z.object({
params: z.object({
id: z.string().uuid(),
}),
body: z.object({
name: z.string().min(1).max(100).optional(),
description: z.string().optional(),
}),
}).passthrough();
```
### 2. Reusable Schemas
```typescript
/**
* TrustRequirement schema matching the TrustRequirement type from trustLevel.types.ts
*
* Accepts two formats:
* - { type: 'number', value: <non-negative integer> }
* - { type: 'level', value: <UUID string> }
*
* This replaces the old plain number format to support both direct numeric thresholds
* and references to trust levels defined in the community.
*/
const trustRequirementSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('number'),
value: z.number().int().min(0),
}),
z.object({
type: z.literal('level'),
value: z.string().uuid(),
}),
]);
const trustTitleSchema = z.object({
name: z.string().min(1).max(50),
minScore: z.number().int().min(0),
});
const metricVisibilitySchema = z.object({
showActiveMembers: z.boolean().optional(),
showWealthGeneration: z.boolean().optional(),
showTrustNetwork: z.boolean().optional(),
showCouncilActivity: z.boolean().optional(),
showNeedsFulfillment: z.boolean().optional(),
showDisputeRate: z.boolean().optional(),
});
```
### 3. Query Parameter Validation with Transformation
```typescript
export const listCommunitiesSchema = z.object({
query: z.object({
page: z.string().transform(Number).pipe(z.number().int().min(0)).optional(),
limit: z.string().transform(Number).pipe(z.number().int().min(1).max(100)).optional(),
}),
}).passthrough();
export const communitySearchQuerySchema = z.object({
query: z.object({
q: z.string().max(200).optional(),
page: z.string().transform(Number).pipe(z.number().int().min(0)).optional(),
limit: z.string().transform(Number).pipe(z.number().int().min(1).max(100)).optional(),
}),
}).passthrough();
```
### 4. Validator Middleware Functions
```typescript
function handleZod(
parse: () => unknown,
res: Response,
next: NextFunction
) {
try {
parse();
next();
} catch (err) {
if (err instanceof z.ZodError) {
return res.status(400).json({
status: 'error',
errors: err.issues,
});
}
next(err);
}
}
export const validateCreateCommunity = (req: Request, res: Response, next: NextFunction) =>
handleZod(() => createCommunitySchema.parse(req), res, next);
export const validateUpdateCommunity = (req: Request, res: Response, next: NextFunction) =>
handleZod(() => updateCommunitySchema.parse(req), res, next);
export const validateGetCommunity = (req: Request, res: Response, next: NextFunction) =>
handleZod(() => getCommunitySchema.parse(req), res, next);
export const validateListCommunities = (req: Request, res: Response, next: NextFunction) =>
handleZod(() => listCommunitiesSchema.parse(req), res, next);
```
### 5. Complex Nested Validation
```typescript
export const createWealthSchema = z.object({
body: z.object({
title: z.string().min(1).max(200),
description: z.string().optional(),
quantity: z.number().int().min(0).optional(),
categoryId: z.string().uuid().optional(),
targetType: z.enum(['community', 'council', 'pool']),
targetId: z.string().uuid().optional(),
minTrustRequired: z.number().int().min(0).optional(),
expiresAt: z.string().datetime().optional().transform(
(val) => val ? new Date(val) : undefined
),
images: z.array(z.string().url()).max(10).optional(),
}).refine(
(data) => {
// If targetType is council or pool, targetId is required
if (data.targetType !== 'community') {
return !!data.targetId;
}
return true;
},
{
message: "targetId is required for council and pool shares",
path: ["targetId"],
}
),
}).passthrough();
```
### 6. Array Validation
```typescript
export const batchUpdateSchema = z.object({
body: z.object({
updates: z.array(z.object({
id: z.string().uuid(),
status: z.enum(['active', 'fulfilled', 'expired', 'cancelled']),
})).min(1).max(50), // Limit batch size
}),
}).passthrough();
```
### 7. Member Management Validators
```typescript
export const getMembersSchema = z.object({
params: z.object({
id: z.string().uuid(),
}),
}).passthrough();
export const removeMemberSchema = z.object({
params: z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
}),
}).passthrough();
export const updateMemberRoleSchema = z.object({
params: z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
}),
body: z.object({
role: z.string().min(1).max(64),
}),
}).passthrough();
```
## Common Zod Patterns
### String Validation
```typescript
z.string() // Any string
z.string().min(1) // Non-empty string
z.string().max(100) // Max length
z.string().email() // Email format
z.string().url() // URL format
z.string().uuid() // UUID format
z.string().datetime() // ISO datetime
z.string().regex(/^[a-z0-9-]+$/) // Custom pattern
```
### Number Validation
```typescript
z.number() // Any number
z.number().int() // Integer only
z.number().min(0) // Minimum value
z.number().max(100) // Maximum value
z.number().positive() // > 0
z.number().nonnegative() // >= 0
```
### Enum Validation
```typescript
z.enum(['option1', 'option2', 'option3'])
z.nativeEnum(MyTypeScriptEnum)
```
### Array Validation
```typescript
z.array(z.string()) // Array of strings
z.array(z.string()).min(1) // Non-empty array
z.array(z.string()).max(10) // Max length
z.array(z.object({ ... })) // Array of objects
```
### Object Validation
```typescript
z.object({
field1: z.string(),
field2: z.number().optional(), // Optional field
field3: z.string().nullable(), // Can be null
})
```
### Discriminated Unions
```typescript
z.discriminatedUnion('type', [
z.object({ type: z.literal('A'), valueA: z.string() }),
z.object({ type: z.literal('B'), valueB: z.number() }),
])
```
### Transformations
```typescript
z.string().transform(Number) // String to number
z.string().transform((val) => val.toLowerCase()) // Custom transform
z.string().datetime().transform((val) => new Date(val)) // String to Date
```
### Refinements (Custom Validation)
```typescript
z.object({ ... }).refine(
(data) => data.field1 !== data.field2,
{
message: "Fields must be different",
path: ["field2"],
}
)
```
## Error Response Format
When validation fails, Zod errors are automatically formatted:
```json
{
"status": "error",
"errors": [
{
"code": "invalid_type",
"expected": "string",
"received": "number",
"path": ["body", "name"],
"message": "Expected string, received number"
},
{
"code": "too_small",
"minimum": 1,
"type": "string",
"inclusive": true,
"path": ["body", "description"],
"message": "String must contain at least 1 character(s)"
}
]
}
```
## Best Practices
1. **Organize by Feature**: One validator file per controller
2. **Reuse Schemas**: Create shared schemas for common patterns
3. **Use .passthrough()**: Allow extra fields to pass through (for future compatibility)
4. **Transform Query Params**: Convert string query params to appropriate types
5. **Validate All Inputs**: params, query, and body
6. **Clear Error Messages**: Use custom messages for business rules
7. **Limit Array Sizes**: Prevent DoS with max array lengths
8. **UUID Validation**: Always validate UUIDs for ID parameters
9. **Optional vs Nullable**: Use .optional() for missing fields, .nullable() for null values
10. **Refinements for Business Rules**: Use .refine() for complex validation logic
## Testing Validators
Validators are tested through integration tests. The validation middleware automatically returns 400 errors with detailed messages.
## Related Skills
- `api-routes` - Using validators in route definitions
- `api-controller` - Receiving validated data
- `api-testing` - Testing validation errors
## Feature Documentation
Before implementing validators, **MUST READ** the relevant feature documentation in `docs/features/` to understand:
- Required and optional fields
- Validation rules and constraints
- Business logic requirements
- Data type specifications