Skip to content

🐛 [RTY-260013]: Decouple MongoDB and Implement Graceful Degradation #13

@recursivezero

Description

@recursivezero

What happened?

To ensure your project remains functional for everyone—even those who don't want to install MongoDB—you need to implement "graceful degradation." This means the app detects the missing library and simply disables those features instead of crashing.

  • Implement Optional Dependency Support: * Move pymongo to [project.optional-dependencies] in pyproject.toml.

  • Add try-except blocks in app/db/data.py to prevent ModuleNotFoundError if the user chooses not to install MongoDB.

  • Enable "Graceful Degradation" (Offline Mode):

  • Ensure the UI still loads if db_available() is False.

  • If no DB is present, the app should still generate short URLs (stored only in the user session/memory) instead of showing an error page.

  • Remove Code Redundancy:

  • Environment Loading: Call load_env() once in main.py instead of in every file.

  • Unified Redirects: Remove the duplicate /{short_code} route from fast_api.py and keep only the one in main.py.

  • Lifecycle Management: Use FastAPI lifespan to connect to the DB once at boot, rather than checking/reconnecting inside individual routes.

  • Clean up create_short_url logic: * Flatten the nested try-except and if-else blocks in main.py to avoid updating request.session in multiple redundant places.

What did you expect to happen?

Problem: Currently, the application has a hard dependency on pymongo. If the package is not installed or the database is unreachable, the application fails to start or crashes during runtime.

Proposed Changes:

1. Move MongoDB to Optional Dependencies

  • Update pyproject.toml to move pymongo from dependencies (or dev-groups) to [project.optional-dependencies].
  • This allows users to install the core app with pip install . and only install database support if needed via pip install .[mongodb].

2. Implement Defensive Imports

  • Modify app/db/data.py to wrap the pymongo import in a try-except ImportError block.
  • Introduce a global boolean MONGO_INSTALLED to track availability.
  • Goal: Prevent ModuleNotFoundError when the app starts without the library.

3. Unified Error Handling & Offline Mode

  • Refactor app/main.py and app/api/fast_api.py to check db_available() before executing queries.
  • If MongoDB is missing or disconnected, the UI should:
  • Display a warning banner: "Running in Limited Mode (No Database)".
  • Allow URL shortening using volatile in-memory generation (short-term session use).
  • Disable the "Recent URLs" table gracefully instead of throwing a 500 error.

4. Remove Code Redundancy

  • Consolidate load_env() calls into a single entry point in main.py.
  • Remove duplicate redirect logic in fast_api.py and let the main app handle all /{short_code} traffic.
  • Move database connection logic into a FastAPI lifespan event to ensure it only attempts to connect once at startup.

5. Update Documentation

  • Update README.md to explain the new installation command for MongoDB users.
  • Add a troubleshooting section for "Offline Mode."

Anything else we need to know?

To help you clean up the code and handle the optional dependency issue, here is a breakdown of the key changes for your app/db/data.py and the points for your GitHub issue.

1. Updated app/db/data.py

This version uses a "Defensive Import" strategy. It won't crash if pymongo is missing; it will simply flag the database as unavailable.

import os
from typing import Any

# --- DEFENSIVE IMPORT ---
try:
    from pymongo import MongoClient
    from pymongo.errors import ServerSelectionTimeoutError
    MONGO_INSTALLED = True
except ImportError:
    # This allows the app to start even if 'pip install pymongo' wasn't run
    MONGO_INSTALLED = False

client: Any = None
db: Any = None
urls: Any = None
url_stats: Any = None

def connect_db():
    global client, db, urls, url_stats

    # 1. Check if the library is even there
    if not MONGO_INSTALLED:
        print("⚠️ pymongo is not installed. Running in NO-DB mode.")
        return False

    # 2. Check if the config is there
    MONGO_URI = os.getenv("MONGO_URI")
    DB_NAME = os.getenv("DATABASE_NAME", "tiny_url")

    if not MONGO_URI:
        print("⚠️ MONGO_URI missing. Running in NO-DB mode.")
        return False

    try:
        # 3. Short timeout so the UI doesn't hang on startup
        client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=2000)
        client.admin.command("ping")

        db = client[DB_NAME]
        urls = db["urls"]
        url_stats = db["url_stats"]

        print(f"✅ MongoDB connected: '{DB_NAME}'")
        return True
    except Exception as e:
        print(f"⚠️ MongoDB connection failed: {e}. Running in NO-DB mode.")
        return False

# REMOVE: connect_db() call from here. 
# It's better to call it in the FastAPI lifespan.

What browsers are you seeing the problem on?

No response

Relevant log output

Contact Details

No response

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions