# FastAPI Server Architecture and Asynchronous Download Handling in SpotifySaver

> Explore the FastAPI server architecture of SpotifySaver. Learn how its DownloadService uses asyncio and BackgroundTasks to handle async downloads efficiently, preventing API response blocking.

- Repository: [Gabriel Baute/spotify-saver](https://github.com/gabrielbaute/spotify-saver)
- Tags: architecture
- Published: 2026-03-02

---

**The gabrielbaute/spotify-saver project implements a thin-controller/thick-service architecture where FastAPI's BackgroundTasks and Python's asyncio thread pools isolate heavy YouTube media downloads from the event loop, enabling concurrent processing without blocking API responses.**

The SpotifySaver repository demonstrates production-ready patterns for building high-throughput file processing APIs in Python. This article examines the FastAPI server architecture and explains how the **DownloadService** handles asynchronous downloads using background tasks and thread-pool offloading to manage CPU-intensive media conversion operations.

## FastAPI Server Architecture Overview

The server follows a classic *thin-controller / thick-service* pattern that separates HTTP concern handling from business logic execution. This design prevents long-running media downloads from exhausting the FastAPI worker thread pool.

### Application Factory Pattern

The entry point `create_app()` in [`spotifysaver/api/app.py`](https://github.com/gabrielbaute/spotify-saver/blob/main/spotifysaver/api/app.py) constructs the FastAPI instance using the factory pattern. This function configures the API metadata, mounts static UI files, applies CORS middleware, and registers the download router:

```python

# spotifysaver/api/app.py

def create_app() -> FastAPI:
    app = FastAPI(
        title="SpotifySaver API",
        description="Download music from Spotify via YouTube Music with metadata preservation",
        version=__version__,
        docs_url="/docs",
        redoc_url="/redoc",
    )
    # Optional static UI mount

    if STATIC_DIR.exists():
        app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
    # CORS

    app.add_middleware(
        CORSMiddleware,
        allow_origins=APIConfig.ALLOWED_ORIGINS,
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )
    # Register router

    app.include_router(download.router, prefix="/api/v1", tags=["download"])
    return app

```

### Router Structure and In-Memory State

The `download` router in [`spotifysaver/api/routers/download.py`](https://github.com/gabrielbaute/spotify-saver/blob/main/spotifysaver/api/routers/download.py) exposes public HTTP endpoints including `/download`, `/download/{id}/status`, and `/inspect`. The router maintains an **in-memory `tasks` dictionary** (lines 29‑31) that stores `DownloadStatus` objects keyed by UUID, acting as a lightweight job queue for tracking asynchronous operations.

### Immediate Response Pattern with BackgroundTasks

When a client initiates a download, the `start_download` endpoint (lines 72‑74) leverages **FastAPI's `BackgroundTasks`** to execute the coroutine *after* sending the HTTP response. This allows the API to return a task ID immediately while the heavy processing continues in the background:

```python

# spotifysaver/api/routers/download.py

@router.post("/download", response_model=DownloadResponse)
async def start_download(request: DownloadRequest, background_tasks: BackgroundTasks):
    task_id = str(uuid.uuid4())
    # Build a pending status entry

    task_status = DownloadStatus(task_id=task_id, status="pending", progress=0,
                                total_tracks=0, completed_tracks=0, failed_tracks=0,
                                started_at=datetime.now().isoformat(),
                                output_format=request.output_format,
                                bit_rate=request.bit_rate)
    tasks[task_id] = task_status

    # Fire the background coroutine

    background_tasks.add_task(download_task, task_id, request)

    return DownloadResponse(task_id=task_id, status="pending",
                            spotify_url=str(request.spotify_url),
                            content_type=content_type,
                            message=f"Download task started for {content_type}")

```

## How DownloadService Handles Asynchronous Downloads

The **DownloadService** in [`spotifysaver/api/services/download_service.py`](https://github.com/gabrielbaute/spotify-saver/blob/main/spotifysaver/api/services/download_service.py) encapsulates all Spotify-to-YouTube-Music download logic. It provides async methods that internally offload CPU-bound work to a thread pool, preventing the event loop from blocking during network I/O and FFmpeg processing.

### The Background Task Orchestrator

The `download_task` coroutine (lines 9‑50 of [`download.py`](https://github.com/gabrielbaute/spotify-saver/blob/main/download.py)) runs outside the request/response cycle. It updates the task status to `"processing"`, instantiates the service, and awaits the download completion:

```python

# spotifysaver/api/routers/download.py

async def download_task(task_id: str, request: DownloadRequest):
    task = tasks[task_id]
    task.status = "processing"

    download_service = DownloadService(
        output_dir=request.output_dir,
        download_lyrics=request.download_lyrics,
        download_cover=request.download_cover,
        generate_nfo=request.generate_nfo,
        output_format=request.output_format,
        bit_rate=request.bit_rate,
    )

    # Progress updates are propagated back to `tasks[task_id]`

    def progress_callback(current, total, track_name):
        task.current_track = track_name
        task.completed_tracks = current - 1
        task.total_tracks = total
        task.progress = int((current / total) * 100) if total else 0

    result = await download_service.download_from_url(
        str(request.spotify_url), progress_callback=progress_callback
    )

    # Finalize status

    task.status = "completed"
    task.progress = 100
    task.completed_tracks = result.get("completed_tracks", 0)
    task.failed_tracks = result.get("failed_tracks", 0)
    task.output_directory = result.get("output_directory")
    task.completed_at = datetime.now().isoformat()

```

### Thread Pool Offloading for Blocking Operations

Inside `DownloadService.download_from_url`, the service inspects the URL type and dispatches to private coroutines (`_download_track`, `_download_album`, `_download_playlist`). Each method offloads the blocking `YouTubeDownloader` to the default thread pool using `loop.run_in_executor` (lines 17‑28 of [`download_service.py`](https://github.com/gabrielbaute/spotify-saver/blob/main/download_service.py)):

```python

# spotifysaver/api/services/download_service.py

async def _download_album(self, album_url: str, progress_callback=None):
    album = self.spotify.get_album(album_url)

    def sync_progress_callback(idx, total, name):
        if progress_callback:
            progress_callback(idx, total, name)

    loop = asyncio.get_event_loop()
    success, total = await loop.run_in_executor(
        None,
        self.downloader.download_album_cli,
        album,
        self.download_lyrics,
        self.output_format,
        self.bit_rate,
        self.generate_nfo,
        self.download_cover,
        sync_progress_callback,
    )
    output_dir = self.downloader._get_album_dir(album)
    return {
        "content_type": "album",
        "completed_tracks": success,
        "failed_tracks": total - success,
        "total_tracks": total,
        "output_directory": str(output_dir),
    }

```

This pattern isolates **yt-dlp** and **FFmpeg** operations (which perform synchronous network requests and CPU-intensive audio conversion) from the async event loop.

### Progress Callback Mechanism

The service accepts a `progress_callback` function that bridges the synchronous downloader and the async task state. The callback executes in the background thread but only mutates the plain Pydantic model stored in the global `tasks` dictionary (lines 25‑31 of [`download.py`](https://github.com/gabrielbaute/spotify-saver/blob/main/download.py)):

```python
def progress_callback(current, total, track_name):
    task.current_track = track_name
    task.completed_tracks = current - 1
    task.total_tracks = total
    task.progress = int((current / total) * 100) if total > 0 else 0

```

Because the dict is only accessed from the single-threaded event loop for read operations (status polling endpoints), this design avoids race conditions without requiring explicit locks.

## Summary

- **Factory Architecture**: The `create_app()` function in [`spotifysaver/api/app.py`](https://github.com/gabrielbaute/spotify-saver/blob/main/spotifysaver/api/app.py) centralizes FastAPI configuration, CORS, and router registration using the application factory pattern.
- **Thin Controller Design**: The download router handles validation and response generation but delegates all processing to `BackgroundTasks`, keeping HTTP workers free.
- **Async/Thread Hybrid**: `DownloadService` uses `asyncio.get_event_loop().run_in_executor()` to run blocking YouTube downloads and FFmpeg conversions in a thread pool while maintaining async method signatures.
- **In-Memory State**: The `tasks` dictionary provides lightweight job tracking without external dependencies, though data persists only for the server's lifetime.
- **Progress Streaming**: A callback mechanism updates `DownloadStatus` objects in real-time, enabling clients to poll `/download/{id}/status` for live progress updates.

## Frequently Asked Questions

### How does FastAPI BackgroundTasks work in this architecture?

FastAPI's `BackgroundTasks` allows the `start_download` endpoint to return a 200 response immediately while scheduling the `download_task` coroutine to run after the response is sent. According to the repository source code, this prevents the HTTP worker thread from being held hostage during the potentially minutes-long media download process.

### Why use `run_in_executor` instead of pure asyncio?

The underlying `YouTubeDownloader` and `yt-dlp` libraries perform synchronous network I/O and CPU-bound FFmpeg audio conversion. As implemented in [`spotifysaver/api/services/download_service.py`](https://github.com/gabrielbaute/spotify-saver/blob/main/spotifysaver/api/services/download_service.py), `run_in_executor` offloads these blocking operations to a thread pool, preventing the async event loop from freezing while maintaining non-blocking API behavior.

### How is download progress communicated to the client?

The service accepts a `progress_callback` function defined in the router (lines 25‑31 of [`download.py`](https://github.com/gabrielbaute/spotify-saver/blob/main/download.py)). As the synchronous downloader completes each track, it invokes this callback, which updates the in-memory `DownloadStatus` object. Clients poll the `/api/v1/download/{task_id}/status` endpoint to retrieve current progress, track names, and completion percentages.

### What happens if the server restarts during a download?

Because the `tasks` dictionary exists only in memory, all download progress and job state are lost if the server process restarts. The repository uses this design for simplicity, but production deployments requiring durability would need to replace the in-memory dict with Redis or a persistent task queue like Celery.