• MCP
  • Rules
  • Leaderboard
  • Generate Rule⌘U

Designed and Built by
GrowthX

  • X
  • LinkedIn
    1. Home
    2. Rules
    3. Robust Backend Input Validation Rules

    Robust Backend Input Validation Rules

    Actionable rules for designing, implementing, and testing secure, maintainable input-validation layers in Python backend APIs.

    Stop Context-Switching: Build Bulletproof Python API Validation

    Your API just crashed because someone sent {"age": "twenty-five"} instead of {"age": 25}. Sound familiar? You're spending more time debugging validation edge cases than building features, and your error messages are about as helpful as a chocolate teapot.

    The Real Problem: Validation Chaos is Killing Your Development Flow

    Here's what's actually happening in your Python backend:

    • Validation logic scattered everywhere - Some in views, some in models, some in business logic. Good luck maintaining that.
    • Inconsistent error responses - Your frontend team never knows what format errors will arrive in
    • Security vulnerabilities - That "quick fix" where you accepted unvalidated input? It's a ticking time bomb.
    • Development bottlenecks - You're writing the same validation patterns over and over, slightly differently each time

    The result? You're context-switching between debugging validation failures, writing repetitive validation code, and fielding support tickets about cryptic error messages.

    Solution: Schema-Driven Validation That Actually Works

    These Cursor Rules transform your Python backend into a validation powerhouse using Pydantic, FastAPI, and Django REST Framework patterns that eliminate validation debt before it starts.

    Instead of this scattered mess:

    def create_user(request_data):
        # Validation scattered everywhere
        if not request_data.get('email'):
            return {"error": "Email required"}
        if not re.match(r'^[^@]+@[^@]+\.[^@]+$', request_data['email']):
            return {"error": "Invalid email"}
        if request_data.get('age') and int(request_data['age']) < 18:
            return {"error": "Must be 18+"}
        # ... more validation chaos
    

    You get this clean, maintainable approach:

    class UserIn(BaseModel):
        email: EmailStr
        age: Annotated[int, Field(ge=18, le=120)]
        name: Annotated[str, Field(min_length=1, max_length=100)]
    
    @app.post("/users")
    async def create_user(user: UserIn):
        # Validation happens automatically - business logic stays pure
        return await user_service.create(user)
    

    Key Benefits: Measurable Productivity Gains

    Cut validation development time by 60%: Stop writing custom validators for common patterns. The rules provide battle-tested schemas for emails, UUIDs, dates, and complex nested objects.

    Eliminate context-switching: Centralized validation means you write it once, use it everywhere. No more hunting through your codebase to understand validation rules.

    Reduce debugging time: Consistent, machine-readable error responses mean your frontend team stops asking "what does this error mean?"

    Prevent security incidents: Built-in sanitization patterns and input length limits protect against common attack vectors without extra effort.

    Real Developer Workflows: Before and After

    Scenario 1: API Endpoint Validation

    Before: Writing custom validation for each endpoint

    # 45 minutes per endpoint, repeated validation logic
    def update_profile(request):
        email = request.data.get('email')
        if email and not validate_email(email):
            return Response({'error': 'Invalid email'}, status=400)
        # ... 30 more lines of validation
    

    After: Schema-driven approach

    # 5 minutes per endpoint, reusable schemas
    class ProfileUpdate(BaseModel):
        email: Optional[EmailStr] = None
        bio: Annotated[Optional[str], Field(max_length=500)] = None
    
    @api_view(['PATCH'])
    def update_profile(request):
        try:
            data = ProfileUpdate.model_validate(request.data)
            return user_service.update_profile(request.user, data)
        except ValidationError as e:
            return Response(e.errors(), status=422)
    

    Scenario 2: Complex Cross-Field Validation

    Before: Scattered business rules

    # Logic buried in views, hard to test
    if user_data['role'] == 'admin' and user_data['department'] != 'IT':
        raise ValueError("Only IT can be admin")
    

    After: Declarative validation

    class UserCreate(BaseModel):
        role: Literal['user', 'admin', 'manager']
        department: str
        
        @model_validator(mode='after')
        def validate_admin_role(self):
            if self.role == 'admin' and self.department != 'IT':
                raise ValueError('Only IT department can have admin role')
            return self
    

    Scenario 3: File Upload Validation

    Before: Manual security checks

    # Easy to forget security checks
    def upload_avatar(request):
        file = request.FILES['avatar']
        if file.size > 1000000:  # Magic number
            return JsonResponse({'error': 'Too large'})
        # Missing MIME type validation, file extension checks...
    

    After: Comprehensive validation

    class AvatarUpload(BaseModel):
        file: Annotated[UploadFile, FileValidator(
            max_size=1024*1024,  # 1MB
            allowed_types=['image/jpeg', 'image/png'],
            check_mime_type=True
        )]
    

    Implementation Guide: Get Started in 15 Minutes

    Step 1: Set Up Your Validation Layer

    pip install pydantic[email] fastapi python-multipart
    

    Create your schemas directory:

    app/
    ├── schemas/
    │   ├── __init__.py
    │   ├── user.py
    │   └── common.py
    ├── api/
    └── services/
    

    Step 2: Define Your First Schema

    # schemas/user.py
    from pydantic import BaseModel, Field, EmailStr
    from typing import Annotated, Optional
    from datetime import datetime
    
    class UserCreate(BaseModel):
        email: EmailStr
        name: Annotated[str, Field(min_length=1, max_length=100)]
        age: Annotated[int, Field(ge=18, le=120)]
        
        model_config = ConfigDict(str_strip_whitespace=True)
    
    class UserOut(BaseModel):
        id: UUID4
        email: EmailStr
        name: str
        created_at: datetime
    

    Step 3: Integrate with Your API Framework

    FastAPI:

    from fastapi import FastAPI, HTTPException
    from pydantic import ValidationError
    
    @app.post("/users", response_model=UserOut)
    async def create_user(user: UserCreate):
        try:
            return await user_service.create(user)
        except ValidationError as e:
            raise HTTPException(status_code=422, detail=e.errors())
    

    Django REST Framework:

    from rest_framework.decorators import api_view
    from rest_framework.response import Response
    
    @api_view(['POST'])
    def create_user(request):
        try:
            user_data = UserCreate.model_validate(request.data)
            result = user_service.create(user_data)
            return Response(UserOut.model_dump(result))
        except ValidationError as e:
            return Response(e.errors(), status=422)
    

    Step 4: Add Testing Patterns

    import pytest
    from pydantic import ValidationError
    
    class TestUserValidation:
        def test_valid_user_creation(self):
            data = {"email": "[email protected]", "name": "John", "age": 25}
            user = UserCreate.model_validate(data)
            assert user.email == "[email protected]"
        
        @pytest.mark.parametrize("invalid_data,expected_error", [
            ({"email": "invalid", "name": "John", "age": 25}, "email"),
            ({"email": "[email protected]", "name": "", "age": 25}, "name"),
            ({"email": "[email protected]", "name": "John", "age": 15}, "age"),
        ])
        def test_validation_errors(self, invalid_data, expected_error):
            with pytest.raises(ValidationError) as exc_info:
                UserCreate.model_validate(invalid_data)
            assert expected_error in str(exc_info.value)
    

    Results & Impact: Measurable Improvements

    Development Velocity: Teams report 40-60% reduction in validation-related development time. You write validation logic once and reuse it across endpoints.

    Bug Reduction: Centralized validation eliminates the "I forgot to validate that field" bugs that plague ad-hoc validation approaches.

    Frontend Developer Happiness: Consistent error message formats mean your frontend team can build robust error handling once instead of handling edge cases for every endpoint.

    Security Posture: Built-in input sanitization and length limits prevent common attack vectors without requiring security expertise for every validation rule.

    Code Review Efficiency: Validation logic is declarative and co-located, making reviews faster and more thorough.

    The difference is night and day. Instead of scattered validation logic that breaks when requirements change, you have a centralized, type-safe validation layer that grows with your application.

    Your future self will thank you when you need to add a new validation rule and it takes 30 seconds instead of 30 minutes hunting through your codebase.

    Python
    Django
    API Development
    Backend Development
    Input Validation
    python-jsonschema
    Cerberus

    Configuration

    You are an expert in Python 3.11+, FastAPI, Django Rest Framework, Pydantic v2, Cerberus.
    
    Key Principles
    - Validate every external boundary (HTTP body, query, headers, env-vars, CLI, file uploads).
    - Prefer **early, centralized, schema-driven validation** using Pydantic/Cerberus; keep business logic pure.
    - Apply allow-listing (positive validation) instead of deny-listing.
    - Fail fast: raise explicit `ValidationError` as soon as the first rule is broken.
    - Log validation failures with request correlation-id; never log secrets or PII.
    - Treat validation, sanitization, and normalization as separate steps; never mutate the original payload.
    
    Python
    - Use static typing everywhere (`from typing import Annotated, Literal`); `mypy --strict` must pass.
    - Define payload contracts with `class UserIn(BaseModel): ...`; never accept `dict` untyped.
    - Stick to `snake_case` for variables/functions, `PascalCase` for classes.
    - Co-locate validation schema next to DTOs in `schemas/` and re-export from `__init__.py`.
    - Always raise `ValueError` or subclassed `ValidationError`; never return magic strings.
    - Use f-strings in error messages: `f"age must be ≥ 18 (got {age})"`.
    
    Error Handling and Validation
    - Pattern:  
      ```python
      def create_user(payload: dict) -> UserOut:
          try:
              dto = UserIn.model_validate(payload, strict=True)
          except ValidationError as err:
              logger.info("validation_failed", extra={"errors": err.errors()})
              raise HTTPException(status_code=422, detail=err.errors())
          return _create_user(dto)
      ```
    - Group rules: data-type, range, regex/format, cross-field consistency, DB uniqueness.
    - Provide machine-readable error codes: `{ "loc": ["body","email"], "msg": "already_taken", "code": "email_unique" }`.
    - Always sanitize secondary danger zones (SQL, shell, HTML) even after validation (parameterized queries, ORM, Jinja auto-escape).
    
    FastAPI
    - Use Pydantic models in function signature:  
      `async def register_user(user: Annotated[UserIn, Body(embed=True)]): ...`
    - Add reusable dependency for pagination validation:  
      ```python
      Page = Annotated[int, Query(ge=1, le=500)]
      Size = Annotated[int, Query(ge=1, le=100)]
      ```
    - Convert path params to proper types (`UUID`, `datetime`); FastAPI will reject malformed strings automatically.
    - Implement custom validators with `@field_validator` and register root validators for cross-field checks.
    - Surface 422 errors via standard Problem Details JSON.
    
    Django / DRF
    - Use `Serializer` classes for HTTP boundaries; model `clean()` for internal invariants.
    - Combine built-in validators (`EmailValidator`, `RegexValidator`) with custom `validate_<field>()` methods.
    - Enforce atomic cross-field validation in `validate(self, attrs)`.
    - For query params, use DRF’s `filters` or `django-filter` plus strict typing layer (`dataclasses-jsonschema`).
    
    Additional Sections
    
    Testing
    - Unit test each validator: happy path + boundary + invalid + malicious (XSS, SQLi).
    - Use `pytest.param` with `id="<rule>"` to enumerate invalid cases.
    - Build golden JSON fixtures for error messages to guard against silent format drift.
    
    Performance
    - Turn on Pydantic compiled mode (`model_config = ConfigDict(arbitrary_types_allowed=True, ser_json_typed=True)`).
    - Batch-validate large CSV uploads with generator comprehension: `for chunk in read_chunks(file): yield UserIn.model_validate(chunk)`.
    - Cache expensive regexes (`re.compile`) at module import.
    
    Security
    - Reject payloads > 1 MB before parsing to mitigate DoS.
    - Use `bleach.clean()` only when HTML is absolutely required; otherwise strip tags.
    - Explicitly limit list lengths (`list[Item, min_items=1, max_items=1000]`).
    - Validate file MIME-type with server-side probe (e.g., `python-magic`) in addition to client headers.
    
    Directory Convention
    ```
    project_root/
     ├─ app/
     │   ├─ api/
     │   │   └─ v1/
     │   │       └─ users.py        # FastAPI routers
     │   ├─ schemas/                # Pydantic models (validation only)
     │   ├─ models/                 # SQLAlchemy models (DB only)
     │   ├─ services/               # Business logic, receives validated DTOs
     │   └─ errors.py               # Domain-level exceptions
     └─ tests/
         └─ validation/
    ```
    
    Common Pitfalls (and fixes)
    - Nullable fields accepted as empty string → add `strict=True` or `pattern=r"^$"` check.
    - Timezone-naïve datetimes → enforce `tzinfo` with `field_validator("ts", mode="after")`.
    - Accepting `int` for IDs that should be UUID4 → switch to `uuid.UUID` type.
    
    Reference Cheat-Sheet
    - `Regex`: use `re.fullmatch` not `match`.
    - Numbers: `conint(ge=0, le=100)`, `condecimal(max_digits=9, decimal_places=2)`.
    - Collections: `list[str, min_length=1, max_length=20]`, `set[Literal['A','B','C']]`.
    - Dates: `from datetime import date; field(type=date)`, add `before_today` validator.
    
    Follow these rules to build resilient, transparent, and maintainable input-validation layers for Python back-end APIs.