mirror of
https://github.com/duthaho/claudekit.git
synced 2026-06-14 06:04:57 +03:00
198 lines
6.2 KiB
Markdown
198 lines
6.2 KiB
Markdown
# Validation Layers Reference
|
|
|
|
Multi-layer validation strategy ensuring no single point of failure.
|
|
|
|
## Overview
|
|
|
|
```
|
|
Request -> [Layer 1: Input] -> [Layer 2: Business] -> [Layer 3: Persistence] -> [Layer 4: Output] -> Response
|
|
```
|
|
|
|
Each layer validates independently. A failure at any layer should produce a clear, actionable error. Never rely on a single layer.
|
|
|
|
## Layer 1: Input Boundary
|
|
|
|
**Purpose**: Reject malformed, oversized, or obviously invalid data at the edge.
|
|
|
|
### What to Validate
|
|
|
|
- Data types and shapes (string, number, object structure)
|
|
- Required vs optional fields
|
|
- String length, numeric ranges, allowed values
|
|
- Format patterns (email, URL, UUID, date)
|
|
- Content-Type headers, encoding
|
|
- File upload size and MIME type
|
|
- Request rate and authentication tokens
|
|
|
|
### Python (FastAPI + Pydantic)
|
|
|
|
```python
|
|
from pydantic import BaseModel, Field, EmailStr
|
|
from fastapi import FastAPI, Query
|
|
|
|
class CreateUserRequest(BaseModel):
|
|
email: EmailStr
|
|
name: str = Field(min_length=1, max_length=200)
|
|
age: int = Field(ge=0, le=150)
|
|
role: Literal["admin", "user", "viewer"]
|
|
|
|
@app.post("/users")
|
|
async def create_user(req: CreateUserRequest):
|
|
# req is already validated by Pydantic
|
|
...
|
|
```
|
|
|
|
### TypeScript (Zod + Express)
|
|
|
|
```typescript
|
|
import { z } from "zod";
|
|
|
|
const CreateUserSchema = z.object({
|
|
email: z.string().email(),
|
|
name: z.string().min(1).max(200),
|
|
age: z.number().int().min(0).max(150),
|
|
role: z.enum(["admin", "user", "viewer"]),
|
|
});
|
|
|
|
app.post("/users", (req, res) => {
|
|
const result = CreateUserSchema.safeParse(req.body);
|
|
if (!result.success) {
|
|
return res.status(400).json({ errors: result.error.issues });
|
|
}
|
|
// result.data is typed and validated
|
|
});
|
|
```
|
|
|
|
### Tools
|
|
|
|
| Language | Library | Purpose |
|
|
|---|---|---|
|
|
| Python | Pydantic, marshmallow, cerberus | Schema validation |
|
|
| TypeScript | Zod, Yup, io-ts, Ajv | Schema validation |
|
|
| Any | JSON Schema | Language-agnostic schema |
|
|
|
|
## Layer 2: Business Logic
|
|
|
|
**Purpose**: Enforce domain rules, state transitions, and authorization.
|
|
|
|
### What to Validate
|
|
|
|
- Business rules (e.g., "cannot cancel a shipped order")
|
|
- State machine transitions (e.g., draft -> published, not draft -> archived)
|
|
- Cross-field dependencies (e.g., "end_date must be after start_date")
|
|
- Authorization (e.g., "only the owner can modify this resource")
|
|
- Resource existence (e.g., "referenced entity must exist")
|
|
- Idempotency and duplicate detection
|
|
|
|
### Python
|
|
|
|
```python
|
|
class OrderService:
|
|
def cancel_order(self, order_id: str, user_id: str) -> Order:
|
|
order = self.repo.get(order_id)
|
|
if order is None:
|
|
raise NotFoundError(f"Order {order_id} not found")
|
|
if order.owner_id != user_id:
|
|
raise ForbiddenError("Only the order owner can cancel")
|
|
if order.status not in ("pending", "confirmed"):
|
|
raise BusinessRuleError(
|
|
f"Cannot cancel order in '{order.status}' status"
|
|
)
|
|
order.status = "cancelled"
|
|
return self.repo.save(order)
|
|
```
|
|
|
|
### TypeScript
|
|
|
|
```typescript
|
|
class OrderService {
|
|
cancelOrder(orderId: string, userId: string): Order {
|
|
const order = this.repo.get(orderId);
|
|
if (!order) throw new NotFoundError(`Order ${orderId} not found`);
|
|
if (order.ownerId !== userId) throw new ForbiddenError("Only the order owner can cancel");
|
|
|
|
const cancellableStatuses = ["pending", "confirmed"] as const;
|
|
if (!cancellableStatuses.includes(order.status)) {
|
|
throw new BusinessRuleError(`Cannot cancel order in '${order.status}' status`);
|
|
}
|
|
order.status = "cancelled";
|
|
return this.repo.save(order);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Guidelines
|
|
|
|
- Keep validation logic in the service/domain layer, not in controllers
|
|
- Use custom exception types that map to HTTP status codes
|
|
- Business rules should be testable independently of HTTP/DB
|
|
|
|
## Layer 3: Data Persistence
|
|
|
|
**Purpose**: Enforce data integrity at the database level as the last line of defense.
|
|
|
|
### What to Validate
|
|
|
|
- NOT NULL constraints
|
|
- UNIQUE constraints (email, username)
|
|
- FOREIGN KEY constraints (referential integrity)
|
|
- CHECK constraints (value ranges, enums)
|
|
- Data types and precision
|
|
- Default values
|
|
|
|
### PostgreSQL Examples
|
|
|
|
```sql
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
email VARCHAR(255) NOT NULL UNIQUE,
|
|
name VARCHAR(200) NOT NULL CHECK (char_length(name) > 0),
|
|
age INTEGER CHECK (age >= 0 AND age <= 150),
|
|
role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'user', 'viewer')),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE orders (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
|
status VARCHAR(20) NOT NULL DEFAULT 'pending'
|
|
CHECK (status IN ('pending', 'confirmed', 'shipped', 'cancelled')),
|
|
total_cents INTEGER NOT NULL CHECK (total_cents >= 0)
|
|
);
|
|
```
|
|
|
|
### Guidelines
|
|
|
|
- Mirror constraints in your ORM (SQLAlchemy `CheckConstraint`, Prisma `@unique`, etc.)
|
|
- Database constraints are the safety net; they catch bugs in application code
|
|
- Always handle constraint violation errors gracefully (unique violation -> 409 Conflict)
|
|
- Use migrations to manage schema changes
|
|
|
|
## Layer 4: Output Boundary
|
|
|
|
**Purpose**: Ensure responses are safe, well-formed, and contain only intended data.
|
|
|
|
### What to Validate
|
|
|
|
- Strip sensitive fields (passwords, internal IDs, tokens)
|
|
- HTML-encode user-generated content to prevent XSS
|
|
- Validate response schema (catch accidental data leaks)
|
|
- Set security headers (Content-Type, X-Content-Type-Options)
|
|
- Limit response size
|
|
|
|
### Techniques
|
|
|
|
- **Python**: Use Pydantic `response_model` to exclude fields not in the response schema
|
|
- **TypeScript**: Create explicit mapper functions (`toUserResponse()`) that pick only safe fields
|
|
- **Headers**: Set `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Content-Security-Policy`
|
|
- **Encoding**: HTML-encode user-generated content before rendering
|
|
|
|
## Layer Interaction Summary
|
|
|
|
| Layer | Catches | If Missing |
|
|
|---|---|---|
|
|
| Input | Malformed data, injection attempts | Bad data flows into business logic |
|
|
| Business | Invalid operations, auth bypass | Violated business rules, data corruption |
|
|
| Persistence | Constraint violations, duplicates | Inconsistent data in database |
|
|
| Output | Data leaks, XSS | Sensitive data exposed to clients |
|