End-to-end web application development workflow with brand theming and standard patterns
Install
npx skillscat add saajunaid/caddis-plugin/webapp-development Install via the SkillsCat registry.
SKILL.md
Web Application Development
Project Context — Read
project-config.mdin the repo root for brand tokens, shared-library paths, and deployment targets.
When to Use
Invoke this skill when:
- Building a new web application from scratch
- Adding a new page to an existing Streamlit multi-page app
- Implementing a full feature (UI + service + data layer)
- Integrating Streamlit frontend with FastAPI backend
- Setting up session state and navigation patterns
- Combining multiple standard patterns into a cohesive app
Steps
Step 1: Create the Page
Every Streamlit page follows this mandatory template:
"""Page Title - Brief description of this page."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from src.components.shared_header import apply_page_config, render_header
# MUST be the FIRST Streamlit call
apply_page_config("PageName", "icon")
render_header(current_page="PageName")
# --- Page content below ---Key Rules:
apply_page_config()MUST be the first Streamlit command- Use fixed header navigation (NOT sidebar)
- Call
render_header()with the current page name
Step 2: Build Components
Extract reusable UI elements into src/components/:
# src/components/kpi_cards.py
import streamlit as st
def render_kpi_row(kpis: list[dict]):
"""Render a row of KPI cards with brand theming."""
cols = st.columns(len(kpis))
for col, kpi in zip(cols, kpis):
with col:
st.metric(
label=kpi["label"],
value=kpi["value"],
delta=kpi.get("delta"),
)Step 3: Implement Service Layer
# src/services/record_service.py
from loguru import logger
import pandas as pd
class RecordService:
def __init__(self, repository):
self._repo = repository
def get_dashboard_data(self, filter_type: str, date_str: str = "") -> dict:
"""Get all data needed for the dashboard page."""
logger.info(f"Loading dashboard data: filter={filter_type}, date={date_str}")
return {
"kpis": self._repo.get_kpi_summary(filter_type, date_str),
"trend": self._repo.get_trend_data(filter_type, date_str),
"distribution": self._repo.get_workbasket_distribution(filter_type, date_str),
}Step 4: Implement Repository Pattern
# src/services/record_repository.py
import pandas as pd
from src.ingestion_config.data_sources import get_data_source_config
class RecordRepository:
def __init__(self):
self._config = get_data_source_config()
self._adapter = self._create_adapter()
def execute_query(self, query_name: str, **params) -> pd.DataFrame:
"""Execute a named query from queries.yaml."""
query_config = self._config.get_query(query_name)
sql = query_config["sql"].format(table=self._config.get_table(query_config["entity"]))
return self._adapter.fetch_dataframe(sql, params.get("params", []))Step 5: Externalize SQL Queries
All SQL goes in src/ingestion_config/queries.yaml:
home_charts:
workbasket_distribution:
description: "Count open cases by workbasket for dashboard"
entity: pega_cases
sql: |
SELECT
ISNULL(WorkbasketHeading, 'Unknown') AS workbasket_heading,
COUNT(*) AS count
FROM {table}
WHERE [Created Date Time] >= ?
AND [Created Date Time] < ?
AND Status = 'Open'
GROUP BY WorkbasketHeading
ORDER BY count DESCStep 6: Apply Brand Theming
# Use brand color constants
BRAND_RED = "#E10A0A"
BRAND_NAVY = "#1F2937"
LIGHT_BG = "#F8F9FA"
CHART_COLORS = [BRAND_RED, BRAND_NAVY, "#6B7280", "#9CA3AF", "#D1D5DB"]
# Load CSS theme
def load_theme():
css_path = Path(__file__).parent.parent / "styles" / "theme.css"
if css_path.exists():
st.markdown(f"<style>{css_path.read_text()}</style>", unsafe_allow_html=True)Step 7: Manage Session State
def init_session_state():
defaults = {
"authenticated": False,
"current_user": None,
"selected_filters": {},
"page_number": 1,
"filter_type": "daily",
}
for key, value in defaults.items():
if key not in st.session_state:
st.session_state[key] = value
def on_filter_change():
"""Reset pagination when filters change."""
st.session_state.page_number = 1Step 8: Add Caching
from datetime import timedelta
@st.cache_data(ttl=timedelta(minutes=15), show_spinner="Loading data...")
def load_dashboard_data(filter_type: str, date_str: str) -> dict:
service = RecordService(get_repo())
return service.get_dashboard_data(filter_type, date_str)
@st.cache_resource
def get_repo():
return RecordRepository()Step 9: Error Handling
from loguru import logger
try:
data = load_dashboard_data(filter_type, date_str)
render_dashboard(data)
except ConnectionError as e:
logger.error(f"Database connection failed: {e}")
st.error("Unable to connect to database. Please try again later.")
except Exception as e:
logger.exception(f"Unexpected error: {e}")
st.error("An unexpected error occurred. Please contact support.")Patterns and Examples
Multi-Page App Structure
app/
├── Home.py # Main entry (streamlit run app/Home.py)
├── pages/
│ ├── 1_Search.py
│ ├── 2_Analytics.py
│ └── 3_Admin.py
├── src/
│ ├── components/
│ │ ├── shared_header.py # Fixed header with brand theming
│ │ └── kpi_cards.py
│ ├── services/
│ │ ├── cached_repository.py
│ │ └── record_service.py
│ ├── models/
│ ├── ingestion_config/
│ │ ├── data_sources.py
│ │ └── queries.yaml
│ └── styles/
│ └── theme.css
└── .envFull Page Example
"""Dashboard - Main overview with KPIs and charts."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
import streamlit as st
from src.components.shared_header import apply_page_config, render_header
from src.components.kpi_cards import render_kpi_row
from src.services.cached_repository import get_chart_data
apply_page_config("Dashboard", "📊")
render_header(current_page="Dashboard")
# Filters
col1, col2 = st.columns([2, 1])
with col1:
filter_type = st.selectbox("Period", ["daily", "weekly", "monthly"],
key="filter_type", on_change=lambda: None)
# KPIs
kpi_data = get_chart_data("kpi_summary", filter_type=filter_type)
render_kpi_row([
{"label": "Open Cases", "value": kpi_data["open_count"]},
{"label": "Avg Resolution", "value": f"{kpi_data['avg_hours']:.1f}h"},
{"label": "SLA Compliance", "value": f"{kpi_data['sla_pct']:.0%}"},
])
# Charts
st.plotly_chart(create_trend_chart(filter_type), use_container_width=True)Checklist
-
apply_page_config()is the first Streamlit call on every page -
render_header()called with correct page name - No sidebar -- using fixed header navigation
- Brand colors applied (60-30-10 rule)
- Service layer separates business logic from UI
- Repository pattern abstracts data access
- SQL externalized to queries.yaml
- Data cached with appropriate TTL
- Session state initialized with defaults
- Error handling with user-friendly messages
- loguru logging for debugging
- Large datasets paginated
- Charts use
update_layout(title=...)nottitle=in px call - Empty states show helpful messages
Related Resources
| Resource | Path |
|---|---|
| Streamlit dev skill | skills/frontend/streamlit-dev/SKILL.md |
| FastAPI dev skill | skills/coding/fastapi-dev/SKILL.md |
| SQL expert skill | the sql skill |
| Caching patterns | skills/coding/caching-patterns/SKILL.md |
| UX design skill | skills/frontend/ux-design/SKILL.md |