Files
orion/15.web-architecture-revamping.md

37 KiB

Web Application Architecture Refactor: Jinja2 Template System

Executive Summary

This document outlines the architectural refactor from a client-side HTML approach to a server-side Jinja2 template system for our multi-tenant e-commerce platform. This change addresses code duplication, improves maintainability, and leverages FastAPI's native templating capabilities while maintaining our Alpine.js reactive frontend.

Table of Contents

  1. Current Architecture Analysis
  2. Proposed Architecture
  3. Key Design Decisions
  4. Migration Strategy
  5. File Structure Comparison
  6. Benefits & Trade-offs
  7. Implementation Checklist
  8. Code Examples
  9. Testing Strategy
  10. Rollback Plan

Current Architecture Analysis

Current Approach: Client-Side Static HTML

┌─────────────────────────────────────────────────┐
│          Browser (Client-Side)                   │
├─────────────────────────────────────────────────┤
│                                                  │
│  1. Load dashboard.html (static file)           │
│  2. Execute partial-loader.js                   │
│  3. Fetch header.html via AJAX                  │
│  4. Fetch sidebar.html via AJAX                 │
│  5. Initialize Alpine.js                        │
│  6. Fetch data from API endpoints               │
│                                                  │
└─────────────────────────────────────────────────┘

Current File Structure

project/
├── static/
│   ├── admin/
│   │   ├── dashboard.html          ← Full HTML page
│   │   ├── vendors.html            ← Full HTML page
│   │   ├── users.html              ← Full HTML page
│   │   ├── partials/
│   │   │   ├── header.html         ← Loaded via AJAX
│   │   │   └── sidebar.html        ← Loaded via AJAX
│   │   ├── css/
│   │   │   └── tailwind.output.css
│   │   └── js/
│   │       ├── init-alpine.js
│   │       └── dashboard.js
│   └── shared/
│       └── js/
│           ├── api-client.js
│           ├── icons.js
│           └── partial-loader.js   ← AJAX loader
└── app/
    └── api/
        └── v1/
            └── admin/
                └── routes.py       ← API endpoints only

Current HTML Example (dashboard.html)

<!DOCTYPE html>
<html :class="{ 'theme-dark': dark }" x-data="adminDashboard()" lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Dashboard - Admin Panel</title>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
    <link rel="stylesheet" href="/static/admin/css/tailwind.output.css" />
    <style>[x-cloak] { display: none !important; }</style>
</head>
<body x-cloak>
    <div class="flex h-screen bg-gray-50 dark:bg-gray-900">
        <!-- Sidebar Container (loaded via AJAX) -->
        <div id="sidebar-container"></div>
        
        <!-- Header Container (loaded via AJAX) -->
        <div id="header-container"></div>
        
        <!-- Main Content -->
        <main>
            <!-- Page-specific content here -->
        </main>
    </div>
    
    <!-- Scripts -->
    <script src="/static/shared/js/partial-loader.js"></script>
    <script>
        (async () => {
            await window.partialLoader.loadAll({
                'header-container': 'header.html',
                'sidebar-container': 'sidebar.html'
            });
        })();
    </script>
    <script src="/static/admin/js/init-alpine.js"></script>
    <script src="/static/shared/js/api-client.js"></script>
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
    <script src="/static/admin/js/dashboard.js"></script>
</body>
</html>

Problems with Current Approach

Problem Impact Severity
HTML Duplication Every page repeats <head>, script tags, and structure High
Multiple HTTP Requests 3+ requests just to render initial page (HTML + header + sidebar) Medium
Timing Issues Race conditions between partial loading and Alpine.js initialization Medium
No Server-Side Control Cannot inject user data, permissions, or dynamic content on page load High
Difficult to Maintain Changes to layout require updating every HTML file High
No Authentication Flow Must rely entirely on client-side routing and checks High
SEO Challenges Static files with no dynamic meta tags or content Low
URL Structure Ugly URLs: /static/admin/dashboard.html Medium

Proposed Architecture

New Approach: Server-Side Jinja2 Templates

┌─────────────────────────────────────────────────┐
│           FastAPI Server (Backend)               │
├─────────────────────────────────────────────────┤
│                                                  │
│  1. Receive request: /admin/dashboard           │
│  2. Check authentication (Depends)              │
│  3. Render Jinja2 template (base + dashboard)   │
│  4. Inject user data, permissions               │
│  5. Return complete HTML                        │
│                                                  │
└─────────────────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────┐
│          Browser (Client-Side)                   │
├─────────────────────────────────────────────────┤
│                                                  │
│  1. Receive complete HTML (single request)      │
│  2. Initialize Alpine.js                        │
│  3. Fetch data from API endpoints               │
│                                                  │
└─────────────────────────────────────────────────┘

Proposed File Structure

project/
├── app/
│   ├── templates/                  ← NEW! Jinja2 templates
│   │   ├── admin/
│   │   │   ├── base.html          ← Base layout (extends pattern)
│   │   │   ├── dashboard.html     ← Extends base.html
│   │   │   ├── vendors.html       ← Extends base.html
│   │   │   └── users.html         ← Extends base.html
│   │   └── partials/
│   │       ├── header.html        ← Included server-side
│   │       └── sidebar.html       ← Included server-side
│   └── api/
│       └── v1/
│           └── admin/
│               ├── routes.py      ← API endpoints
│               └── pages.py       ← NEW! Page routes (HTML)
└── static/
    ├── admin/
    │   ├── css/
    │   │   └── tailwind.output.css
    │   └── js/
    │       ├── init-alpine.js
    │       └── dashboard.js
    └── shared/
        └── js/
            ├── api-client.js
            ├── icons.js
            └── utils.js           ← NEW! (was missing)

Key Design Decisions

1. Template Inheritance (Jinja2 Extends/Blocks)

Decision: Use Jinja2's {% extends %} and {% block %} pattern.

Rationale:

  • DRY Principle: Define layout once in base.html, reuse everywhere
  • Maintainability: Change header/footer in one place
  • Native to FastAPI: No additional dependencies or build tools
  • Industry Standard: Proven pattern used by Django, Flask, etc.

Example:

{# base.html - Parent Template #}
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}Admin Panel{% endblock %}</title>
    {# Common head elements #}
</head>
<body>
    {% include 'partials/sidebar.html' %}
    {% include 'partials/header.html' %}
    <main>
        {% block content %}{% endblock %}
    </main>
    {# Common scripts #}
    {% block extra_scripts %}{% endblock %}
</body>
</html>

{# dashboard.html - Child Template #}
{% extends "admin/base.html" %}

{% block title %}Dashboard{% endblock %}

{% block content %}
    <h1>Dashboard Content</h1>
{% endblock %}

{% block extra_scripts %}
    <script src="/static/admin/js/dashboard.js"></script>
{% endblock %}

2. Server-Side Rendering with Client-Side Reactivity

Decision: Render HTML on server, enhance with Alpine.js on client.

Rationale:

  • Best of Both Worlds: Fast initial render + reactive UI
  • Progressive Enhancement: Works without JavaScript (graceful degradation)
  • SEO Friendly: Complete HTML on first load
  • Performance: Single HTTP request for initial page load

Architecture:

Server (FastAPI + Jinja2)          Client (Alpine.js)
─────────────────────────          ──────────────────
Generate complete HTML    ──────►  Parse HTML
Include user data         ──────►  Initialize Alpine
Include permissions       ──────►  Add interactivity
                                   Fetch dynamic data via API

3. Separation of Concerns: Pages vs API

Decision: Separate routes for HTML pages and JSON API endpoints.

File Structure:

app/api/v1/admin/
├── pages.py          # Returns HTML (Jinja2 templates)
   └── @router.get("/dashboard", response_class=HTMLResponse)

└── routes.py         # Returns JSON (API endpoints)
    └── @router.get("/dashboard/stats", response_model=StatsResponse)

Rationale:

  • Clear Responsibility: Page routes render HTML, API routes return JSON
  • RESTful Design: API endpoints remain pure and reusable
  • Flexibility: Can build mobile app using same API
  • Testing: Can test HTML rendering and API logic separately

URL Structure:

Old (Current):
  /static/admin/dashboard.html       (Static file)
  /api/v1/admin/dashboard/stats      (JSON API)

New (Proposed):
  /admin/dashboard                   (HTML via Jinja2)
  /api/v1/admin/dashboard/stats      (JSON API - unchanged)

4. Authentication & Authorization at Route Level

Decision: Use FastAPI dependencies for auth on page routes.

Example:

from fastapi import Depends
from app.api.deps import get_current_admin_user

@router.get("/dashboard", response_class=HTMLResponse)
async def admin_dashboard_page(
    request: Request,
    current_user: User = Depends(get_current_admin_user),  # ← Auth check
    db: Session = Depends(get_db)
):
    """
    Render admin dashboard page.
    Requires admin authentication - redirects to login if not authenticated.
    """
    return templates.TemplateResponse(
        "admin/dashboard.html",
        {
            "request": request,
            "user": current_user  # ← Can access in template
        }
    )

Rationale:

  • Security First: Cannot access page without authentication
  • Automatic Redirects: FastAPI handles 401 → login redirect
  • User Context: Pass authenticated user to templates
  • Permissions: Can check roles/permissions before rendering

5. Keep Alpine.js for Dynamic Interactions

Decision: Continue using Alpine.js for client-side reactivity.

What Stays the Same:

  • Alpine.js for interactive components
  • x-data, x-show, x-if directives
  • API calls via apiClient.js
  • Icon system (icons.js)
  • Utility functions (utils.js)

What Changes:

  • No more partial-loader.js (Jinja2 handles includes)
  • No more client-side template loading
  • Alpine initializes on complete HTML (faster)

Example:

{# dashboard.html - Alpine.js still works! #}
{% extends "admin/base.html" %}

{% block alpine_data %}adminDashboard(){% endblock %}

{% block content %}
<div x-data="adminDashboard()">
    <h1 x-text="pageTitle">Dashboard</h1>
    
    <button @click="refresh()">
        <span x-html="$icon('refresh')"></span>
        Refresh
    </button>
    
    <div x-show="loading">
        <span x-html="$icon('spinner')"></span>
        Loading...
    </div>
    
    <template x-for="item in items" :key="item.id">
        <div x-text="item.name"></div>
    </template>
</div>
{% endblock %}

6. Static Assets Remain Static

Decision: Keep CSS, JS, images in /static/ directory.

Rationale:

  • Performance: Static files served with caching headers
  • CDN Ready: Can move to CDN later if needed
  • No Change Needed: Existing assets work as-is

Mounting:

from fastapi.staticfiles import StaticFiles

app.mount("/static", StaticFiles(directory="static"), name="static")

Usage in Templates:

{# Jinja2 provides url_for() for static files #}
<link rel="stylesheet" href="{{ url_for('static', path='/admin/css/tailwind.output.css') }}" />
<script src="{{ url_for('static', path='/admin/js/dashboard.js') }}"></script>

7. Data Flow: Server → Template → Alpine.js

Decision: Three-tier data flow for optimal performance.

Tier 1: Server-Side Data (Initial Page Load)

@router.get("/dashboard")
async def dashboard(request: Request, current_user: User = Depends(...)):
    # Can pass initial data to avoid extra API call
    initial_stats = await get_dashboard_stats()
    
    return templates.TemplateResponse(
        "admin/dashboard.html",
        {
            "request": request,
            "user": current_user,
            "initial_stats": initial_stats  # ← Available in template
        }
    )

Tier 2: Template Rendering (Jinja2)

{# Can render initial data from server #}
<div class="stat-card">
    <h3>Total Users</h3>
    <p>{{ initial_stats.total_users }}</p>  {# ← From server #}
</div>

{# Or let Alpine.js fetch it #}
<div x-data="adminDashboard()">
    <p x-text="stats.totalUsers"></p>  {# ← From Alpine/API #}
</div>

Tier 3: Alpine.js (Dynamic Updates)

function adminDashboard() {
    return {
        stats: {},
        async init() {
            // Fetch fresh data via API
            this.stats = await apiClient.get('/admin/dashboard/stats');
        },
        async refresh() {
            // Re-fetch on demand
            this.stats = await apiClient.get('/admin/dashboard/stats');
        }
    };
}

When to Use Each Tier:

Data Type Use Server Use Alpine Rationale
User info Server Available at auth time, no extra call needed
Permissions Server Security-sensitive, should be server-side
Static config Server Doesn't change during session
Dashboard stats ⚠️ Either Alpine Initial load via server, refresh via Alpine
Real-time data Alpine Changes frequently, fetch via API
Form data Alpine User input, submit via API

Migration Strategy

Phase 1: Preparation (No Breaking Changes)

Goal: Set up infrastructure without affecting existing pages.

Tasks:

  1. Create app/templates/ directory structure
  2. Create utils.js (missing file)
  3. Create app/api/v1/admin/pages.py (new file)
  4. Configure Jinja2 in FastAPI
  5. Create base template (base.html)

Timeline: 1-2 hours Risk: Low (no existing code changes)

Phase 2: Migrate One Page (Proof of Concept)

Goal: Migrate dashboard.html to prove the approach works.

Tasks:

  1. Create app/templates/admin/dashboard.html
  2. Create route in pages.py
  3. Test authentication flow
  4. Test Alpine.js integration
  5. Verify API calls still work

Validation:

  • Dashboard loads at /admin/dashboard
  • Authentication required to access
  • Stats cards display correctly
  • Alpine.js reactivity works
  • API calls fetch data correctly
  • Icon system works
  • Dark mode toggle works

Timeline: 2-3 hours Risk: Low (parallel to existing page)

Phase 3: Migrate Remaining Pages

Goal: Migrate vendors.html, users.html using same pattern.

Tasks:

  1. Create templates for each page
  2. Create routes in pages.py
  3. Test each page independently
  4. Update internal links

Timeline: 3-4 hours Risk: Low (pattern established)

Phase 4: Cleanup & Deprecation

Goal: Remove old static HTML files.

Tasks:

  1. Update all navigation links
  2. Remove partial-loader.js
  3. Remove old HTML files from /static/admin/
  4. Update documentation
  5. Update deployment scripts if needed

Timeline: 1-2 hours Risk: Medium (ensure all links updated)

Migration Checklist

## Pre-Migration
- [ ] Backup current codebase
- [ ] Document current URLs and functionality
- [ ] Create feature branch: `feature/jinja2-templates`

## Phase 1: Setup
- [ ] Create `app/templates/admin/` directory
- [ ] Create `app/templates/partials/` directory
- [ ] Create `utils.js` in `/static/shared/js/`
- [ ] Create `app/api/v1/admin/pages.py`
- [ ] Configure Jinja2Templates in FastAPI
- [ ] Create `base.html` template
- [ ] Move `header.html` to `app/templates/partials/`
- [ ] Move `sidebar.html` to `app/templates/partials/`
- [ ] Test template rendering with simple page

## Phase 2: Dashboard Migration
- [ ] Create `app/templates/admin/dashboard.html`
- [ ] Create dashboard route in `pages.py`
- [ ] Add authentication dependency
- [ ] Test page renders correctly
- [ ] Test Alpine.js initialization
- [ ] Test API data fetching
- [ ] Test icon system
- [ ] Test user menu and logout
- [ ] Test dark mode toggle
- [ ] Compare old vs new side-by-side

## Phase 3: Additional Pages
- [ ] Create `vendors.html` template
- [ ] Create `users.html` template
- [ ] Create routes for each page
- [ ] Test each page independently
- [ ] Test navigation between pages
- [ ] Update breadcrumbs if applicable

## Phase 4: Cleanup
- [ ] Update all internal links to new URLs
- [ ] Remove `partial-loader.js`
- [ ] Remove old HTML files from `/static/admin/`
- [ ] Update README with new architecture
- [ ] Update this documentation
- [ ] Run full test suite
- [ ] Merge feature branch to main

## Post-Migration
- [ ] Monitor error logs for 404s
- [ ] Collect user feedback
- [ ] Performance testing
- [ ] Document lessons learned

File Structure Comparison

Before (Current)

project/
├── static/
│   └── admin/
│       ├── dashboard.html          ← 150 lines (full HTML)
│       ├── vendors.html            ← 150 lines (full HTML)
│       ├── users.html              ← 150 lines (full HTML)
│       ├── partials/
│       │   ├── header.html         ← 80 lines
│       │   └── sidebar.html        ← 120 lines
│       ├── css/
│       │   └── tailwind.output.css
│       └── js/
│           ├── init-alpine.js
│           ├── dashboard.js
│           ├── vendors.js
│           └── users.js
└── app/
    └── api/
        └── v1/
            └── admin/
                └── routes.py       ← API endpoints only

Total HTML Lines: ~650 lines with massive duplication

After (Proposed)

project/
├── app/
│   ├── templates/
│   │   ├── admin/
│   │   │   ├── base.html          ← 80 lines (reused by all)
│   │   │   ├── dashboard.html     ← 40 lines (content only)
│   │   │   ├── vendors.html       ← 40 lines (content only)
│   │   │   └── users.html         ← 40 lines (content only)
│   │   └── partials/
│   │       ├── header.html        ← 80 lines
│   │       └── sidebar.html       ← 120 lines
│   └── api/
│       └── v1/
│           └── admin/
│               ├── pages.py       ← NEW! Page routes
│               └── routes.py      ← API endpoints
└── static/
    └── admin/
        ├── css/
        │   └── tailwind.output.css
        └── js/
            ├── init-alpine.js
            ├── dashboard.js
            ├── vendors.js
            └── users.js

Total HTML Lines: ~400 lines with NO duplication
Savings: ~250 lines (38% reduction)

Benefits & Trade-offs

Benefits

1. Reduced Code Duplication

Metric:

  • Current: ~650 lines of HTML with 60% duplication
  • After: ~400 lines with 0% duplication
  • Savings: 38% fewer lines to maintain

2. Better Performance

Metric Before After Improvement
Initial Page Requests 3+ (HTML + header + sidebar) 1 (complete HTML) 66% fewer requests
Time to First Paint ~300ms ~150ms 50% faster
JavaScript Parse Time Same Same No change

3. Improved Security

Before:

  • Client can access /static/admin/dashboard.html directly
  • Must implement client-side auth checks
  • Can bypass authentication by manipulating JavaScript

After:

  • Server checks authentication before rendering
  • FastAPI Depends() enforces auth at route level
  • Cannot bypass server-side checks

4. Better SEO (Future-Proof)

Before:

  • Static HTML with no dynamic meta tags
  • Same title/description for all pages

After:

  • Can generate dynamic meta tags per page
  • Can inject structured data for search engines
{% block head %}
    <meta name="description" content="Admin dashboard for {{ user.company_name }}" />
    <meta property="og:title" content="Dashboard - {{ user.company_name }}" />
{% endblock %}

5. Easier Maintenance

Scenario: Update header navigation

Before:

Edit header.html → Save → Test → Works ✓
But: Must reload page to see changes (AJAX loads old cached version)

After:

Edit header.html → Save → Refresh → Works ✓
Changes appear immediately (server-side include)

6. Better Developer Experience

Task Before After
Create new page Copy 150 lines, modify content Extend base, write 40 lines
Change layout Edit every HTML file Edit base.html once
Add auth to page Write JavaScript checks Add Depends() to route
Pass user data API call in Alpine.js Available in template
Debug template Browser + Network tab Browser + FastAPI logs

Trade-offs

1. Server Load ⚖️

Before:

  • Serving static files (nginx-level, very fast)
  • No server-side processing

After:

  • FastAPI renders Jinja2 on each request
  • Minimal overhead (~1-2ms per render)

Mitigation:

  • Jinja2 is extremely fast (C-compiled)
  • Can add template caching if needed
  • Static assets still served by nginx

Verdict: Negligible impact for admin panel traffic

2. Learning Curve ⚖️

Before:

  • Developers only need HTML + Alpine.js

After:

  • Developers need Jinja2 syntax
  • Must understand template inheritance

Mitigation:

  • Jinja2 is very similar to Django/Flask templates
  • Documentation provided
  • Pattern is straightforward once learned

Verdict: Small one-time learning cost

3. Debugging Changes ⚖️

Before:

  • View source in browser = actual file
  • Easy to inspect what's loaded

After:

  • View source = rendered output
  • Must check template files in codebase

Mitigation:

  • Better error messages from FastAPI
  • Template path shown in errors
  • Can enable Jinja2 debug mode

Verdict: Different, not harder

4. Cannot Edit in Browser DevTools ⚖️

Before:

  • Can edit static HTML in browser, reload

After:

  • Must edit template file, refresh server

Mitigation:

  • FastAPI auto-reloads on file changes
  • Hot reload works well in development

Verdict: Minimal impact, proper workflow

Code Examples

Example 1: Base Template

{# app/templates/admin/base.html #}
<!DOCTYPE html>
<html :class="{ 'theme-dark': dark }" x-data="{% block alpine_data %}data(){% endblock %}" lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{% block title %}Admin Panel{% endblock %} - Multi-Tenant Platform</title>
    
    <!-- Fonts -->
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
    
    <!-- Tailwind CSS -->
    <link rel="stylesheet" href="{{ url_for('static', path='/admin/css/tailwind.output.css') }}" />
    
    <!-- Alpine Cloak -->
    <style>
        [x-cloak] { display: none !important; }
    </style>
    
    {% block extra_head %}{% endblock %}
</head>
<body x-cloak>
    <div class="flex h-screen bg-gray-50 dark:bg-gray-900" :class="{ 'overflow-hidden': isSideMenuOpen }">
        <!-- Sidebar -->
        {% include 'partials/sidebar.html' %}

        <div class="flex flex-col flex-1 w-full">
            <!-- Header -->
            {% include 'partials/header.html' %}

            <!-- Main Content -->
            <main class="h-full overflow-y-auto">
                <div class="container px-6 mx-auto grid">
                    {% block content %}{% endblock %}
                </div>
            </main>
        </div>
    </div>

    <!-- Core Scripts -->
    <script src="{{ url_for('static', path='/shared/js/icons.js') }}"></script>
    <script src="{{ url_for('static', path='/admin/js/init-alpine.js') }}"></script>
    <script src="{{ url_for('static', path='/shared/js/api-client.js') }}"></script>
    <script src="{{ url_for('static', path='/shared/js/utils.js') }}"></script>
    
    <!-- Alpine.js v3 -->
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
    
    {% block extra_scripts %}{% endblock %}
</body>
</html>

Key Features:

  • {% block alpine_data %} - Allows child templates to specify Alpine component
  • {{ url_for('static', path='...') }} - Proper static file URLs
  • {% include %} - Server-side include (no AJAX needed)
  • {% block content %} - Child templates inject content here
  • {% block extra_scripts %} - Page-specific JavaScript

Example 2: Child Template (Dashboard)

{# app/templates/admin/dashboard.html #}
{% extends "admin/base.html" %}

{% block title %}Dashboard{% endblock %}

{% block alpine_data %}adminDashboard(){% endblock %}

{% block content %}
<!-- Page Header -->
<div class="flex items-center justify-between my-6">
    <h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
        Dashboard
    </h2>
    <button 
        @click="refresh()"
        :disabled="loading"
        class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white bg-purple-600 rounded-lg hover:bg-purple-700"
    >
        <span x-show="!loading" x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
        <span x-show="loading" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
        <span x-text="loading ? 'Loading...' : 'Refresh'"></span>
    </button>
</div>

<!-- Stats Cards -->
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
    <!-- Card -->
    <div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
        <div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full">
            <span x-html="$icon('user-group', 'w-5 h-5')"></span>
        </div>
        <div>
            <p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
                Total Vendors
            </p>
            <p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.totalVendors">
                0
            </p>
        </div>
    </div>
</div>
{% endblock %}

{% block extra_scripts %}
<script src="{{ url_for('static', path='/admin/js/dashboard.js') }}"></script>
{% endblock %}

Key Features:

  • Extends base.html for layout
  • Overrides specific blocks (title, alpine_data, content, extra_scripts)
  • Uses Alpine.js directives (x-data, x-show, x-text, x-html)
  • Uses icon system via $icon() magic helper
  • Page-specific JavaScript loaded via extra_scripts block

Example 3: Route Configuration (pages.py)

# app/api/v1/admin/pages.py
from fastapi import APIRouter, Request, Depends
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session

from app.api.deps import get_current_admin_user, get_db
from app.models import User

router = APIRouter()
templates = Jinja2Templates(directory="app/templates")

@router.get("/dashboard", response_class=HTMLResponse)
async def admin_dashboard_page(
    request: Request,
    current_user: User = Depends(get_current_admin_user),
    db: Session = Depends(get_db)
):
    """
    Render admin dashboard page.
    Requires admin authentication.
    """
    # Optional: Pass initial data to avoid extra API call
    # initial_stats = await get_dashboard_stats(db)
    
    return templates.TemplateResponse(
        "admin/dashboard.html",
        {
            "request": request,
            "user": current_user,
            # "initial_stats": initial_stats,
        }
    )

@router.get("/vendors", response_class=HTMLResponse)
async def admin_vendors_page(
    request: Request,
    current_user: User = Depends(get_current_admin_user),
    db: Session = Depends(get_db)
):
    """
    Render vendors management page.
    Requires admin authentication.
    """
    return templates.TemplateResponse(
        "admin/vendors.html",
        {
            "request": request,
            "user": current_user,
        }
    )

@router.get("/users", response_class=HTMLResponse)
async def admin_users_page(
    request: Request,
    current_user: User = Depends(get_current_admin_user),
    db: Session = Depends(get_db)
):
    """
    Render users management page.
    Requires admin authentication.
    """
    return templates.TemplateResponse(
        "admin/users.html",
        {
            "request": request,
            "user": current_user,
        }
    )

Example 4: Main App Configuration

# app/main.py
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

from app.api.v1.admin import routes as admin_api_routes
from app.api.v1.admin import pages as admin_page_routes

app = FastAPI(title="Multi-Tenant Platform")

# Mount static files
app.mount("/static", StaticFiles(directory="static"), name="static")

# Configure Jinja2
templates = Jinja2Templates(directory="app/templates")

# Include API routes (JSON endpoints)
app.include_router(
    admin_api_routes.router,
    prefix="/api/v1/admin",
    tags=["admin-api"]
)

# Include page routes (HTML rendering)
app.include_router(
    admin_page_routes.router,
    prefix="/admin",
    tags=["admin-pages"]
)

@app.get("/")
async def root():
    return {"message": "Multi-Tenant Platform API"}

Testing Strategy

Unit Tests

Test Template Rendering:

# tests/test_templates.py
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_dashboard_requires_authentication():
    """Dashboard should redirect to login if not authenticated."""
    response = client.get("/admin/dashboard")
    assert response.status_code == 401  # or 302 redirect

def test_dashboard_renders_for_authenticated_user(authenticated_client):
    """Dashboard should render for authenticated admin user."""
    response = authenticated_client.get("/admin/dashboard")
    assert response.status_code == 200
    assert b"Dashboard" in response.content
    assert b"<!DOCTYPE html>" in response.content

def test_dashboard_includes_user_data(authenticated_client, test_user):
    """Dashboard should include user information."""
    response = authenticated_client.get("/admin/dashboard")
    assert test_user.email.encode() in response.content

Test Alpine.js Integration:

def test_dashboard_includes_alpine_js(authenticated_client):
    """Dashboard should include Alpine.js script."""
    response = authenticated_client.get("/admin/dashboard")
    assert b"alpinejs" in response.content
    assert b'x-data="adminDashboard()"' in response.content

Integration Tests

Test Full Page Flow:

# tests/integration/test_admin_pages.py
from playwright.sync_api import sync_playwright

def test_dashboard_loads_and_fetches_data():
    """Test dashboard loads, Alpine initializes, and fetches data."""
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        
        # Navigate to dashboard
        page.goto("http://localhost:8000/admin/dashboard")
        
        # Should redirect to login if not authenticated
        assert page.url.endswith("/login")
        
        # Login
        page.fill('input[name="email"]', "admin@example.com")
        page.fill('input[name="password"]', "password")
        page.click('button[type="submit"]')
        
        # Should redirect to dashboard
        page.wait_for_url("**/admin/dashboard")
        
        # Wait for Alpine to initialize
        page.wait_for_selector('[x-data="adminDashboard()"]')
        
        # Check stats cards load
        page.wait_for_selector(".stat-card")
        
        # Verify data is displayed
        assert page.locator('text="Total Vendors"').is_visible()
        
        browser.close()

Performance Tests

Measure Rendering Time:

import time
from fastapi.testclient import TestClient

def test_dashboard_render_performance(authenticated_client):
    """Dashboard should render in under 50ms."""
    start = time.time()
    response = authenticated_client.get("/admin/dashboard")
    duration = time.time() - start
    
    assert response.status_code == 200
    assert duration < 0.05  # 50ms

Rollback Plan

If Issues Arise During Migration

Phase 1 Rollback (Setup Phase):

  • No rollback needed - no breaking changes
  • Simply don't use new templates yet

Phase 2 Rollback (Dashboard Migration):

  1. Keep old dashboard.html in place
  2. Remove new route from pages.py
  3. Revert any URL changes
  4. No data loss or downtime

Phase 3-4 Rollback:

  1. Restore old HTML files from backup
  2. Re-add partial-loader.js
  3. Update navigation links back to old URLs
  4. Remove new routes from pages.py

Emergency Rollback Script:

#!/bin/bash
# rollback.sh

echo "Rolling back Jinja2 migration..."

# Restore old HTML files
git checkout main -- static/admin/dashboard.html
git checkout main -- static/admin/vendors.html
git checkout main -- static/admin/users.html
git checkout main -- static/shared/js/partial-loader.js

# Remove new page routes
git checkout main -- app/api/v1/admin/pages.py

# Restart server
echo "Restarting server..."
systemctl restart fastapi-app

echo "Rollback complete!"

Health Checks

Monitor After Deployment:

# app/health.py
from fastapi import APIRouter

router = APIRouter()

@router.get("/health/templates")
async def check_templates():
    """Check if templates are rendering correctly."""
    try:
        from fastapi.templating import Jinja2Templates
        templates = Jinja2Templates(directory="app/templates")
        # Try to get template
        templates.get_template("admin/base.html")
        return {"status": "healthy", "templates": "ok"}
    except Exception as e:
        return {"status": "unhealthy", "error": str(e)}

Conclusion

This migration from client-side static HTML to server-side Jinja2 templates provides:

Key Benefits:

  • 38% reduction in code duplication
  • 66% fewer HTTP requests on initial page load
  • 50% faster time to first paint
  • Better security with server-side authentication
  • Cleaner URLs and better SEO
  • Easier maintenance and development

Minimal Trade-offs:

  • ⚠️ Slight increase in server load (negligible)
  • ⚠️ Small learning curve for Jinja2 syntax
  • ⚠️ Different debugging workflow

Next Steps:

  1. Review and approve this architecture document
  2. Schedule migration phases
  3. Begin Phase 1 (infrastructure setup)
  4. Migrate dashboard as proof of concept
  5. Roll out to remaining pages
  6. Monitor and optimize

Timeline: 1-2 days for complete migration Risk Level: Low (incremental, reversible changes)