Building a Real-time Dashboard with FastAPI and Svelte

Last updated March 26th, 2025

In this tutorial, you'll learn how to build a real-time analytics dashboard using FastAPI and Svelte. We'll use server-sent events (SSE) to stream live data updates from FastAPI to our Svelte frontend, creating an interactive dashboard that updates in real-time.

Final app:

Dashboard final

Dependencies:

  • Svelte v5.23.2
  • SvelteKit v2.19.0
  • Node v22.14.0
  • npm v11.2.0
  • FastAPI v0.115.11
  • Python v3.13.2

Objectives

By the end of this tutorial, you should be able to:

  1. Set up a FastAPI backend with real-time data streaming capabilities
  2. Create a modern Svelte application using SvelteKit
  3. Implement server-sent events (SSE) for real-time data updates
  4. Build interactive charts and graphs with Svelte components
  5. Handle real-time data updates efficiently in the frontend

What Are We Building?

We'll create an analytics dashboard that displays mock sensor data in real-time. The dashboard will include:

  • A line chart showing temperature trends
  • A gauge chart displaying current humidity levels
  • Real-time status indicators
  • Historical data view

This is a practical example that can be adapted for any application requiring real-time data visualization.

Project Setup

Let's start by creating our project structure, open a terminal and run the following commands:

$ mkdir svelte-fastapi-dashboard
$ cd svelte-fastapi-dashboard

We'll organize our project with two main directories:

svelte-fastapi-dashboard/
├── backend/
└── frontend/

Let's begin with the backend setup...

FastAPI Backend

First, let's set up our backend environment. Create and navigate to the backend directory:

$ mkdir backend
$ cd backend

Create and activate a virtual environment:

$ python -m venv venv
$ source venv/bin/activate
$ export PYTHONPATH=$PWD

Install the required dependencies:

(venv)$ pip install fastapi==0.115.11 uvicorn==0.34.0 sse-starlette==2.2.1

We're using sse-starlette for server-sent events support in FastAPI.

Create the following directory structure in the "backend" folder:

backend/
├── app/
│   ├── __init__.py
│   ├── api.py
│   └── sensor.py
└── main.py

Let's implement a mock sensor data generator in backend/app/sensor.py:

import random
from datetime import datetime
from typing import Dict


class SensorData:
    def __init__(self):
        self.min_temp = 18.0
        self.max_temp = 26.0
        self.min_humidity = 30.0
        self.max_humidity = 65.0

    def generate_reading(self) -> Dict:
        """Generate a mock sensor reading."""
        return {
            "timestamp": datetime.now().isoformat(),
            "temperature": round(random.uniform(self.min_temp, self.max_temp), 1),
            "humidity": round(random.uniform(self.min_humidity, self.max_humidity), 1),
            "status": random.choice(["normal", "warning", "critical"])
        }

Now, let's create our FastAPI application in backend/app/api.py:

import asyncio
import json

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sse_starlette.sse import EventSourceResponse

from .sensor import SensorData


app = FastAPI()
sensor = SensorData()


# Configure CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],  # Svelte dev server
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/")
async def root():
    return {"message": "Welcome to the Sensor Dashboard API"}


@app.get("/current")
async def get_current_reading():
    """Get the current sensor reading."""
    return sensor.generate_reading()


@app.get("/stream")
async def stream_data():
    """Stream sensor data using server-sent events."""
    async def event_generator():
        while True:
            data = sensor.generate_reading()
            yield {
                "event": "sensor_update",
                "data": json.dumps(data)
            }
            await asyncio.sleep(2)  # Update every 2 seconds

    return EventSourceResponse(event_generator())

In this section, we've created a FastAPI application that streams sensor data using server-sent events (SSE). The /current endpoint returns the current sensor reading, and the /stream endpoint streams sensor data updates in real-time.

Finally, create the entry point in backend/main.py:

import uvicorn


if __name__ == "__main__":
    uvicorn.run("app.api:app", host="0.0.0.0", port=8000, reload=True)

Start the server:

(venv)$ python main.py

Your API should now be running at http://localhost:8000. You can check the API documentation at http://localhost:8000/docs. After visiting the http://localhost:8000 you should see the following output:

{
"message": "Welcome to the Sensor Dashboard API"
}

Svelte Frontend

Now let's create our Svelte application using SvelteKit. Navigate back to the project root and create the frontend:

$ cd ..
$ npx [email protected] create frontend

When prompted, select the following options:

  • Which template would you like? › SvelteKit minimal
  • Add type checking with TypeScript? › Yes, using TypeScript syntax
  • What would you like to add to your project? (use arrow keys/space bar):
    • ✓ prettier
    • ✓ eslint
    • ✓ vitest
  • Which package manager do you want to install dependencies with? › npm

Install the dependencies:

$ cd frontend
$ npm install

We'll also need some additional packages for our dashboard:

Let's create our main dashboard layout. Replace the contents of frontend/src/routes/+page.svelte with:

<script lang="ts">
  import { onMount } from 'svelte';
  import type { SensorReading } from '$lib/types';

  let currentReading: SensorReading | null = null;
  let eventSource: EventSource;

  onMount(async () => {
    // Initial data fetch
    const response = await fetch('http://localhost:8000/current');
    currentReading = await response.json();

    // Set up SSE connection
    eventSource = new EventSource('http://localhost:8000/stream');
    eventSource.addEventListener('sensor_update', (event) => {
      currentReading = JSON.parse(event.data);
    });

    return () => {
      eventSource.close();
    };
  });
</script>

<main class="container">
  <h1>Sensor Dashboard</h1>

  {#if currentReading}
    <div class="dashboard-grid">
      <div class="card">
        <h2>Temperature</h2>
        <p class="reading">{currentReading.temperature}°C</p>
      </div>

      <div class="card">
        <h2>Humidity</h2>
        <p class="reading">{currentReading.humidity}%</p>
      </div>

      <div class="card">
        <h2>Status</h2>
        <p class="status {currentReading.status}">{currentReading.status}</p>
      </div>
    </div>
  {/if}
</main>

<style>
  .container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
  }

  .dashboard-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 1rem;
    margin-top: 2rem;
  }

  .card {
    background: #fff;
    padding: 1.5rem;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  }

  .reading {
    font-size: 2rem;
    font-weight: bold;
    margin: 1rem 0;
  }

  .status {
    text-transform: uppercase;
    font-weight: bold;
  }

  .status.normal { color: #2ecc71; }
  .status.warning { color: #f1c40f; }
  .status.critical { color: #e74c3c; }
</style>

Create a new file frontend/src/lib/types.ts to define our types:

export interface SensorReading {
  timestamp: string;
  temperature: number;
  humidity: number;
  status: 'normal' | 'warning' | 'critical';
}

With the backend running in one terminal window, start the Svelte development server:

$ npm run dev

Your dashboard should now be accessible at http://localhost:5173, showing real-time sensor data updates!

Dashboard initial

What's happening in this code?

  1. Component Structure: The dashboard component follows a typical Svelte structure with three main sections:
    1. Script (logic)
    2. Template (HTML)
    3. Style (CSS)
  2. Data Management
    1. Uses TypeScript for type safety
    2. Maintains two key pieces of state:
      1. currentReading: Stores the latest sensor data
      2. eventSource: Manages the real-time connection
  3. Real-time Data Flow:
    1. Initial Load: Fetches current sensor data when component mounts
    2. Live Updates: Establishes SSE connection for real-time updates
    3. Cleanup: Properly closes connection when component is destroyed

The temperature, humidity, and status are pulled from the backend from values defined in sensor.py and shown in the dashboard.

Real-time Charts

Let's enhance our dashboard with interactive charts using Chart.js. First, create a new components directory:

$ mkdir src/lib/components

Create a new component for our temperature chart in frontend/src/lib/components/TemperatureChart.svelte:

<script lang="ts">
  import { onMount } from 'svelte';
  import Chart from 'chart.js/auto';
  import type { SensorReading } from '$lib/types';

  export let data: SensorReading[];
  let canvas: HTMLCanvasElement;
  let chart: Chart;

  $: if (chart && data) {
    chart.data.labels = data.map(reading => {
      const date = new Date(reading.timestamp);
      return date.toLocaleTimeString();
    });
    chart.data.datasets[0].data = data.map(reading => reading.temperature);
    chart.update();
  }

  onMount(() => {
    chart = new Chart(canvas, {
      type: 'line',
      data: {
        labels: [],
        datasets: [{
          label: 'Temperature (°C)',
          data: [],
          borderColor: '#3498db',
          tension: 0.4,
          fill: false
        }]
      },
      options: {
        responsive: true,
        animation: {
          duration: 0 // Disable animations for real-time updates
        },
        scales: {
          y: {
            beginAtZero: false,
            suggestedMin: 15,
            suggestedMax: 30
          }
        }
      }
    });

    return () => {
      chart.destroy();
    };
  });
</script>

<canvas bind:this={canvas}></canvas>

This component creates a real-time temperature line chart using Chart.js. When mounted, it initializes an empty chart with the appropriate styling and scale settings. The reactive statement, ($:), watches for changes in the data array, automatically updating the chart with new temperature readings and converting timestamps to readable time formats.

Create a similar component for humidity in frontend/src/lib/components/HumidityGauge.svelte:

<script lang="ts">
  import { onMount } from 'svelte';
  import Chart from 'chart.js/auto';

  export let value: number;
  let canvas: HTMLCanvasElement;
  let chart: Chart;

  $: if (chart && value !== undefined) {
    chart.data.datasets[0].data = [value];
    chart.update();
  }

  onMount(() => {
    chart = new Chart(canvas, {
      type: 'doughnut',
      data: {
        datasets: [{
          data: [value],
          backgroundColor: ['#2ecc71'],
          circumference: 180,
          rotation: 270,
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          tooltip: {
            enabled: false
          }
        }
      }
    });

    return () => {
      chart.destroy();
    };
  });
</script>

<div class="gauge-container">
  <canvas bind:this={canvas}></canvas>
  <div class="gauge-value">{value}%</div>
</div>

<style>
  .gauge-container {
    position: relative;
    height: 200px;
  }

  .gauge-value {
    position: absolute;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    font-size: 1.5rem;
    font-weight: bold;
  }
</style>

In this section, we created a gauge chart to visualize humidity levels using Chart.js' doughnut type with custom configuration (circumference: 180, rotation: 270). It accepts a numeric value prop that both powers the gauge visualization and displays as text beneath the chart.

Now, let's update our main page to include historical data and the new chart components. Update frontend/src/routes/+page.svelte:

<script lang="ts">
  import { onMount } from 'svelte';
  import TemperatureChart from '$lib/components/TemperatureChart.svelte';
  import HumidityGauge from '$lib/components/HumidityGauge.svelte';
  import type { SensorReading } from '$lib/types';

  let currentReading: SensorReading | null = null;
  let historicalData: SensorReading[] = [];
  let eventSource: EventSource;

  onMount(async () => {
    // Initial data fetch
    const response = await fetch('http://localhost:8000/current');
    currentReading = await response.json();
    historicalData = [currentReading];

    // Set up SSE connection
    eventSource = new EventSource('http://localhost:8000/stream');
    eventSource.addEventListener('sensor_update', (event) => {
      currentReading = JSON.parse(event.data);
      historicalData = [...historicalData, currentReading].slice(-30); // Keep last 30 readings
    });

    return () => {
      eventSource.close();
    };
  });
</script>

<main class="container">
  <h1>Sensor Dashboard</h1>

  {#if currentReading}
    <div class="dashboard-grid">
      <div class="card span-2">
        <h2>Temperature History</h2>
        <TemperatureChart data={historicalData} />
      </div>

      <div class="card">
        <h2>Current Humidity</h2>
        <HumidityGauge value={currentReading.humidity} />
      </div>

      <div class="card">
        <h2>System Status</h2>
        <div class="status-container">
          <div class="status-indicator {currentReading.status}"></div>
          <p class="status-text">{currentReading.status}</p>
          <p class="timestamp">Last updated: {new Date(currentReading.timestamp).toLocaleTimeString()}</p>
        </div>
      </div>
    </div>
  {/if}
</main>

<style>
  .container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
  }

  .dashboard-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1rem;
    margin-top: 2rem;
  }

  .card {
    background: #fff;
    padding: 1.5rem;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  }

  .span-2 {
    grid-column: span 2;
  }

  .status-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
  }

  .status-indicator {
    width: 80px;
    height: 80px;
    border-radius: 50%;
    margin: 1rem 0;
  }

  .status-indicator.normal { background-color: #2ecc71; }
  .status-indicator.warning { background-color: #f1c40f; }
  .status-indicator.critical { background-color: #e74c3c; }

  .status-text {
    text-transform: uppercase;
    font-weight: bold;
  }

  .timestamp {
    color: #666;
    font-size: 0.9rem;
  }

  @media (max-width: 768px) {
    .dashboard-grid {
      grid-template-columns: 1fr;
    }

    .span-2 {
      grid-column: auto;
    }
  }
</style>

Now in the browser you should see the following:

Dashboard initial

Settings and Alerts

Alert Notifications

Let's create a notification system for when sensor values exceed certain thresholds. Create a new component in frontend/src/lib/components/AlertBanner.svelte:

<script lang="ts">
  import { fade } from 'svelte/transition';

  export let message: string;
  export let type: 'warning' | 'critical' = 'warning';
</script>

{#if message}
  <div class="alert {type}" transition:fade>
    <span class="alert-icon">⚠️</span>
    {message}
  </div>
{/if}

<style>
  .alert {
    position: fixed;
    top: 1rem;
    right: 1rem;
    padding: 1rem;
    border-radius: 4px;
    color: white;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    z-index: 1000;
  }

  .warning {
    background-color: #f1c40f;
  }

  .critical {
    background-color: #e74c3c;
  }

  .alert-icon {
    font-size: 1.2rem;
  }
</style>

The AlertBanner component implements conditional UI rendering using Svelte's {#if} block pattern to toggle alert visibility. When there is an alert message, the component displays a banner with an icon and a message.

Settings Panel

Finally, let's add a settings panel to configure alert thresholds in frontend/src/lib/components/SettingsPanel.svelte:

<script lang="ts">
  import { fade } from 'svelte/transition';

  export let tempThreshold = 25;
  export let humidityThreshold = 60;
  let isOpen = false;

  function updateSettings() {
    // Save the settings
    tempThreshold = tempThreshold;
    humidityThreshold = humidityThreshold;
    // Close the panel
    isOpen = false;
  }
</script>

<div class="settings-container">
  <button class="settings-button" on:click={() => isOpen = !isOpen}>
    ⚙️ Settings
  </button>

  {#if isOpen}
    <div class="settings-panel" transition:fade>
      <h3>Alert Thresholds</h3>
      <div class="setting-group">
        <label>
          Temperature (°C)
          <input type="number" bind:value={tempThreshold} min="0" max="40" step="0.5">
        </label>
      </div>
      <div class="setting-group">
        <label>
          Humidity (%)
          <input type="number" bind:value={humidityThreshold} min="0" max="100" step="5">
        </label>
      </div>
      <div class="button-group">
        <button class="cancel" on:click={() => isOpen = false}>Cancel</button>
        <button class="save" on:click={updateSettings}>Save</button>
      </div>
    </div>
  {/if}
</div>

<style>
  .settings-container {
    position: fixed;
    bottom: 1rem;
    right: 1rem;
    z-index: 1000;
  }

  .settings-button {
    background: #2c3e50;
    color: white;
    border: none;
    padding: 0.5rem 1rem;
    border-radius: 4px;
    cursor: pointer;
    transition: background-color 0.2s;
  }

  .settings-button:hover {
    background: #34495e;
  }

  .settings-panel {
    position: absolute;
    bottom: 100%;
    right: 0;
    background: white;
    padding: 1.5rem;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.2);
    margin-bottom: 0.5rem;
    min-width: 280px;
    color: #2c3e50;
  }

  h3 {
    margin: 0 0 1rem 0;
    font-size: 1.2rem;
    color: #2c3e50;
  }

  .setting-group {
    margin: 1rem 0;
  }

  label {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    font-size: 0.9rem;
    color: #34495e;
  }

  input {
    padding: 0.5rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 1rem;
    width: 100%;
  }

  input:focus {
    outline: none;
    border-color: #3498db;
    box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
  }

  .button-group {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
    margin-top: 1.5rem;
  }

  .button-group button {
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 0.9rem;
    transition: opacity 0.2s;
  }

  .button-group button:hover {
    opacity: 0.9;
  }

  .save {
    background: #2ecc71;
    color: white;
  }

  .cancel {
    background: #95a5a6;
    color: white;
  }
</style>

In the SettingsPanel component, we implemented a configurable interface for alert threshold management using bind:value directives for two-way data binding with the parent component. The component maintains internal state (isOpen) to control panel visibility while exporting props (tempThreshold, humidityThreshold) that function as both inputs and outputs. We used Svelte's built-in transition:fade for DOM animations.

Update the main page, frontend/src/routes/+page.svelte, to include alert handling and the settings panel:

<script lang="ts">
  import { onMount } from 'svelte';
  import TemperatureChart from '$lib/components/TemperatureChart.svelte';
  import HumidityGauge from '$lib/components/HumidityGauge.svelte';
  import SettingsPanel from '$lib/components/SettingsPanel.svelte';
  import AlertBanner from '$lib/components/AlertBanner.svelte';
  import type { SensorReading } from '$lib/types';

  let currentReading: SensorReading | null = null;
  let historicalData: SensorReading[] = [];
  let eventSource: EventSource;
  let tempThreshold = 25;
  let humidityThreshold = 60;
  let alertMessage = '';
  let alertType: 'warning' | 'critical' = 'warning';

  onMount(async () => {
    // Initial data fetch
    const response = await fetch('http://localhost:8000/current');
    currentReading = await response.json();
    historicalData = [currentReading];

    // Set up SSE connection
    eventSource = new EventSource('http://localhost:8000/stream');
    eventSource.addEventListener('sensor_update', (event) => {
      currentReading = JSON.parse(event.data);
      historicalData = [...historicalData, currentReading].slice(-30); // Keep last 30 readings
      if (currentReading) {
        checkAlertConditions(currentReading);
      }
    });

    return () => {
      eventSource.close();
    };
  });

  function checkAlertConditions(reading: SensorReading) {
    if (reading.temperature > tempThreshold) {
      alertMessage = `High temperature detected: ${reading.temperature}°C`;
      alertType = 'critical';
    } else if (reading.humidity > humidityThreshold) {
      alertMessage = `High humidity detected: ${reading.humidity}%`;
      alertType = 'warning';
    } else {
      alertMessage = '';
    }
  }

  // Reactive statement to check alerts when thresholds or readings change
  $: if (currentReading) {
    checkAlertConditions(currentReading);
  }
</script>

<AlertBanner message={alertMessage} type={alertType} />

<main class="container">
  <h1>Sensor Dashboard</h1>

  {#if currentReading}
    <div class="dashboard-grid">
      <div class="card span-2">
        <h2>Temperature History</h2>
        <TemperatureChart data={historicalData} />
      </div>

      <div class="card">
        <h2>Current Humidity</h2>
        <HumidityGauge value={currentReading.humidity} />
      </div>

      <div class="card">
        <h2>System Status</h2>
        <div class="status-container">
          <div class="status-indicator {currentReading.status}"></div>
          <p class="status-text">{currentReading.status}</p>
          <p class="timestamp">Last updated: {new Date(currentReading.timestamp).toLocaleTimeString()}</p>
        </div>
      </div>
    </div>
  {/if}
</main>

<style>
  .container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
  }

  .dashboard-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1rem;
    margin-top: 2rem;
  }

  .card {
    background: #fff;
    padding: 1.5rem;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  }

  .span-2 {
    grid-column: span 2;
  }

  .status-container {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
  }

  .status-indicator {
    width: 80px;
    height: 80px;
    border-radius: 50%;
    margin: 1rem 0;
  }

  .status-indicator.normal { background-color: #2ecc71; }
  .status-indicator.warning { background-color: #f1c40f; }
  .status-indicator.critical { background-color: #e74c3c; }

  .status-text {
    text-transform: uppercase;
    font-weight: bold;
  }

  .timestamp {
    color: #666;
    font-size: 0.9rem;
  }

  @media (max-width: 768px) {
    .dashboard-grid {
      grid-template-columns: 1fr;
    }

    .span-2 {
      grid-column: auto;
    }
  }
</style>

<SettingsPanel bind:tempThreshold bind:humidityThreshold />

The final dashboard combines all components: temperature history chart, humidity gauge, system status indicator, alert banner, and settings panel.

Your real-time dashboard is now complete with charts, alerts, and configurable settings! The final result should look something like this:

Dashboard final

Conclusion

In this tutorial, we've built a real-time dashboard using FastAPI and Svelte. We've covered:

  • Setting up a FastAPI backend with SSE for real-time data streaming
  • Creating a responsive Svelte frontend with interactive charts
  • Implementing real-time data updates and historical data tracking
  • Adding an alert system for monitoring threshold violations
  • Creating a configurable settings panel

This dashboard can serve as a foundation for more complex monitoring applications. Some potential enhancements could include:

  • Adding authentication
  • Persisting historical data in a database
  • Adding more visualization types
  • Implementing websocket communication for bi-directional real-time updates
  • Adding export functionality for historical data

The complete source code for this project is available on GitHub.

Amir Tadrisi

Amir Tadrisi

Amir loves building educational applications and has been doing so since 2013. He's a full-stack developer who loves the challenges of working with cutting-edge technologies like Python, Django, React, and Next.js to create modern, scalable learning management systems.

Share this tutorial

Featured Course

Test-Driven Development with FastAPI and Docker

In this course, you'll learn how to build, test, and deploy a text summarization service with Python, FastAPI, and Docker. The service itself will be exposed via a RESTful API and deployed to Heroku with Docker.

Featured Course

Test-Driven Development with FastAPI and Docker

In this course, you'll learn how to build, test, and deploy a text summarization service with Python, FastAPI, and Docker. The service itself will be exposed via a RESTful API and deployed to Heroku with Docker.