-
Notifications
You must be signed in to change notification settings - Fork 6
Initial Python Backend Scaffolding: database setup, models, error handling and two endpoints #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
tmcneil-mdb
merged 7 commits into
mongodb:main
from
tmcneil-mdb:python-scaffolding-setup
Oct 24, 2025
Merged
Changes from 5 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
afa5824
feat(backend): init FastAPI Movies API with utils, error handling and…
tmcneil-mdb ff0369e
Merge branch 'mongodb:main' into python-backend-setup
tmcneil-mdb 4cacbca
chore: Restructured project and added inital unit test setup
tmcneil-mdb a8ac9f3
feat(api): add create_models_batch endpoint and added createMovieRequ…
tmcneil-mdb 68ff1f1
chore: removing incomplete unit tests
tmcneil-mdb 89d3ea3
fix(pr feedback & dirty response data):
tmcneil-mdb 38e4973
chore: Adopting pip-tools pinning strategy
tmcneil-mdb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| #------------------------- | ||
| # Python / FastAPI backend | ||
| #------------------------- | ||
|
|
||
| # Byte-compiled / optimized files | ||
| __pycache__/ | ||
| *.py[cod] | ||
| *$py.class | ||
|
|
||
| # Virtual environment | ||
| .venv/ | ||
|
|
||
| # Environment variables | ||
| .env | ||
|
|
||
| #MacOS files | ||
| .DS_Store |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| # Welcome to the Python Backend | ||
|
|
||
| TOC | ||
| 1. [Overview](#overview) | ||
| 2. [Getting Setup](#getting-setup) | ||
| 3. [Learning FastAPI](#learning-fastapi) | ||
| 4. [Exploring the Codebase](#exploring-the-codebase) | ||
| 5. [Feature Parity Status](#feature-parity-status) | ||
| 6. [Utilities and Nice to Know](#nice-to-know) | ||
|
|
||
|
|
||
| ## Overview | ||
| We're porting our Express backend to Python/FastAPI to achieve **functional parity** - same endpoints, same responses, same MongoDB operations. This allows one frontend to work with multiple backends. | ||
|
|
||
|
|
||
| **Important**: We're not rewriting - we're replicating behavior exactly. So this means we are figuring out the pythonic way of doing things. You will find that we can accomplish things with Fast in fewer lines of code than in Express. | ||
|
|
||
| ## Getting Setup | ||
| 0. Install Python if you don't already have it. (*Depending on your version of python, you might need to use python3 and pip3 vs python and pip*) | ||
| 1. Clone from my fork of the main repo. | ||
| ```sh | ||
| https://github.com/tmcneil-mdb/docs-sample-apps.git | ||
| ``` | ||
|
|
||
| 2. Checkout my branch | ||
| ```sh | ||
| git checkout python-backend-setup | ||
| ``` | ||
|
|
||
| 4. Verify you are on the right branch | ||
| ```sh | ||
| git branch | ||
| ``` | ||
| 5. In the root directory. /python, make a virtual environment. | ||
| ```sh | ||
| python -m venv .venv | ||
| ``` | ||
| 6. Activate the virtual environment | ||
| ```sh | ||
| source .venv/bin/activate | ||
| ``` | ||
| 7. Navigate back to /python, install the required packages. | ||
| ```sh | ||
| pip install -r requirements.txt | ||
| ``` | ||
| 8. Create an .env file at the /python root. | ||
| It should have this format: | ||
| ```python | ||
| MONGO_URI="place your connection string here" | ||
| MONGO_DB="sample_mflix" | ||
| ``` | ||
| 9. Navigate back to the root and start the server. | ||
| ```sh | ||
| fastapi dev main.py or uvicorn main:app --reload | ||
| ``` | ||
| (NOTE: THESE WONT WORK IF THE VIRTUAL ENVIRONMENT IS NOT ACTIVATED) | ||
| 10. Click the link in the terminal or visit ```localhost:8000/docs``` | ||
| 11. Try visiting ```localhost:8000/api/movies```. | ||
|
|
||
| If the setup ran correctly, you should see data 🎉 | ||
|
|
||
| *I recommend having your atlas instance up to explore your data and query the db directly.* | ||
|
|
||
| **Troubleshooting**: If commands fail, ensure your virtual environment is activated (you should see `(.venv)` in your terminal prompt). | ||
|
|
||
|
|
||
| ## Learning FastAPI | ||
| Before diving into the repo, I suggest spending some time with the official [FastAPI tutorial](https://fastapi.tiangolo.com/tutorial/). | ||
|
|
||
| You only need to read up to the 'Request Body' section to get comfortable. *The query parameters and path parameters section can be read later.* | ||
|
|
||
| Key Takeaways: | ||
| - Decorators (@app.get, @app.post,etc.) | ||
| - Query, path, and body parameters | ||
| - Pydantic validation/ serialization | ||
| - Automatic JSON responses | ||
| - Built-in docs and testing at '/docs' | ||
|
|
||
|
|
||
| ## Exploring the Codebase | ||
| Once you have completed the tutorial, I would suggest exploring the code and just noticing the differences between Express and Python. The apps are setup similarly but there are some small differences. | ||
|
|
||
| ### Architecture | ||
| |Layer|Purpose|Express Equivalent| Differences| | ||
| |:---|:-------|:----------|:------| | ||
| |Routes <br> `src/routers/movies.py`| Defines all /movies endpoints (GET, POST, PUT, etc.)| /controllers/movieController.ts |The movies.ts file inside the routes file in the Express backend it actually wiring up the endpoints. Fast handles this for us in the main.py in one line. ```app.include_router(movies.router, prefix="/api/movies", tags=["movies"])```| | ||
| |Models <br> `src/models/models.py`| Pydantic schema for validating and serializing request/response data.| /types/index.ts|There are some differences in how the models are constructed. Take note on how nested classes are handled.| | ||
| |Utils <br> `src/utils/errorHandler.py`| Centralized utilities for responses, error handling and MongoDB exception mapping.|utils/errorHandler.ts |Express requires devs to write exception handling and validation on their own. Pydanic handles the validation and exception handling is a bit cleaner in Fast. You will notice the most differences here.| | ||
| |Database <br>`src/database/mongo_client.py`| Handles the connection to the db|/config/database.ts| *The current database file does not have feature parity with the Express version*| | ||
|
|
||
|
|
||
| ## Feature Parity Status | ||
| |Feature|Status|Owner|Notes| | ||
| |:------|:-----|:----|:----| | ||
| |Global Exception Handling| DONE| - |Found in Utils | | ||
| |JSON Response Matching| DONE | - |Found in Utils| | ||
| |CRUD- ```insertOne()```| Not Started| Angela| - | | ||
| |CRUD- ```insertMany()```| Not Started| Taylor| - | | ||
| |CRUD- ```findOne()```| Not Started| Angela| - | | ||
| |CRUD- ```find()```| DONE| - | Found in movies.py as ```get_all_movies()``` This is a good function to look at to understand query parameters, requests and responses. Your functions will be simpler than this. But that is a good base to start with.| | ||
| |CRUD- ```updateOne()```| Not Started| Angela| - | | ||
| |CRUD- ```updateMany()```| Not Started| Taylor| - | | ||
| |CRUD- ```deleteOne()```| Not Started| Angela| - | | ||
| |CRUD- ```deleteMany()```| Not Started| Taylor| -| | ||
| |CRUD- ```findOne()```| Not Started| Angela| - | | ||
| |CRUD- ```findOneAndDelete()```| Not Started| Angela| This one is a bit harder but I figure this could be a fun challenge.| | ||
|
|
||
|
|
||
| ## Nice to Know | ||
| ### Utilities (errorHandler.py) | ||
| This module provides a parity replacement for the Express errorHandler.ts file. It provides the same response shapes and error handling and removes the middleware. | ||
|
|
||
| - create_success_response(data,message) - creates a success response with the same shape as the Express version. | ||
| - create_error_response(message,code,details) - creates a error response with the same shape as the Express version. | ||
| - parse_mongo_exception(exc) - ensures PyMongo exceptions return JSON identical to the Express Version | ||
| - register_error_handlers - hooks our error system into the Fast app | ||
|
|
||
| Fast automatically handles async and validation errors, so there is no need for asyncHandler or validateRequiredFields from the TS version. | ||
|
|
||
| ### Useful Links | ||
| - [Main Repo]( https://github.com/mongodb/docs-sample-apps/tree/main ) | ||
| - [Sample App Scoping Doc](https://docs.google.com/document/d/12dROckw_Cp0ku2IIGku-ch7MvEuBPo0V4Gs-5wQgeHQ/edit?tab=t.0) | ||
| - [Sample App Project Description & Breakdown Doc](https://docs.google.com/document/d/1xv2dmcNrT-HYk5TBE-KtVDPmW0274rpBZ0_0QmC66ac/edit?tab=t.0#heading=h.ki9tatw08ilc) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| from fastapi import FastAPI | ||
| from src.routers import movies | ||
| from src.utils.errorHandler import register_error_handlers | ||
|
|
||
| app = FastAPI() | ||
| register_error_handlers(app) | ||
| app.include_router(movies.router, prefix="/api/movies", tags=["movies"]) | ||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
| #------------------------------------ | ||
| # Testing error endpoints. Will be removed later | ||
| #------------------------------------ | ||
|
|
||
| ''' | ||
| @app.get("/") | ||
| async def root(): | ||
| return {"message": "Backend is running!"} | ||
|
|
||
| @app.get("/test-duplicate") | ||
| async def test_duplicate(): | ||
| from pymongo.errors import DuplicateKeyError | ||
| raise DuplicateKeyError("This is a test duplicate key error.") | ||
|
|
||
| @app.get("/test-generic") | ||
| async def test_generic(): | ||
| from pymongo.errors import PyMongoError | ||
| raise PyMongoError("This is a test generic pymongo error.") | ||
| ''' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| annotated-types==0.7.0 | ||
| anyio==4.11.0 | ||
| certifi==2025.10.5 | ||
| click==8.3.0 | ||
| dnspython==2.8.0 | ||
| email-validator==2.3.0 | ||
| fastapi==0.119.0 | ||
| fastapi-cli==0.0.13 | ||
| fastapi-cloud-cli==0.3.1 | ||
| h11==0.16.0 | ||
| httpcore==1.0.9 | ||
| httptools==0.7.1 | ||
| httpx==0.28.1 | ||
| idna==3.11 | ||
| iniconfig==2.3.0 | ||
| Jinja2==3.1.6 | ||
| markdown-it-py==4.0.0 | ||
| MarkupSafe==3.0.3 | ||
| mdurl==0.1.2 | ||
| packaging==25.0 | ||
| pluggy==1.6.0 | ||
| pydantic==2.12.2 | ||
|
tmcneil-mdb marked this conversation as resolved.
Outdated
|
||
| pydantic_core==2.41.4 | ||
| Pygments==2.19.2 | ||
| pymongo==4.15.3 | ||
| pytest==8.4.2 | ||
| pytest-asyncio==1.2.0 | ||
| python-dotenv==1.1.1 | ||
| python-multipart==0.0.20 | ||
| PyYAML==6.0.3 | ||
| rich==14.2.0 | ||
| rich-toolkit==0.15.1 | ||
| rignore==0.7.1 | ||
| sentry-sdk==2.42.0 | ||
|
tmcneil-mdb marked this conversation as resolved.
Outdated
|
||
| shellingham==1.5.4 | ||
| sniffio==1.3.1 | ||
| starlette==0.48.0 | ||
| typer==0.19.2 | ||
|
tmcneil-mdb marked this conversation as resolved.
Outdated
|
||
| typing-inspection==0.4.2 | ||
| typing_extensions==4.15.0 | ||
| urllib3==2.5.0 | ||
| uvicorn==0.37.0 | ||
| uvloop==0.21.0 | ||
|
tmcneil-mdb marked this conversation as resolved.
Outdated
|
||
| watchfiles==1.1.1 | ||
| websockets==15.0.1 | ||
|
tmcneil-mdb marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| from pymongo import AsyncMongoClient | ||
| from dotenv import load_dotenv | ||
| import os | ||
|
|
||
| load_dotenv() | ||
|
|
||
| client = AsyncMongoClient(os.getenv("MONGO_URI")) | ||
| db =client[os.getenv("MONGO_DB")] | ||
|
tmcneil-mdb marked this conversation as resolved.
Outdated
|
||
|
|
||
| def get_collection(name:str): | ||
| return db[name] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| from pydantic import BaseModel, Field | ||
| from datetime import datetime | ||
| from typing import Optional, TypeVar, Generic, Any | ||
|
|
||
|
|
||
| T = TypeVar("T") | ||
|
|
||
| class Awards(BaseModel): | ||
| wins: Optional[int] = None | ||
| nominations: Optional[int] = None | ||
| text: Optional[str] = None | ||
|
|
||
| class Imbd(BaseModel): | ||
|
tmcneil-mdb marked this conversation as resolved.
Outdated
|
||
| rating: Optional[float] = None | ||
| votes: Optional[int] = None | ||
| id: Optional[int] = None | ||
|
|
||
| class Movie(BaseModel): | ||
| id: Optional[str] = Field(alias="_id") | ||
| title: str | ||
| year: Optional[int] = None | ||
| plot: Optional[str] = None | ||
| fullplot: Optional[str] = None | ||
| released: datetime = None | ||
|
tmcneil-mdb marked this conversation as resolved.
Outdated
|
||
| runtime: Optional[int] = None | ||
| poster: Optional[str] = None | ||
| genres: Optional[list[str]] = None | ||
| directors: Optional[list[str]] = None | ||
| writers: Optional[list[str]] = None | ||
| cast: Optional[list[str]] = None | ||
| countries: Optional[list[str]] = None | ||
| languages: Optional[list[str]] = None | ||
| rated: Optional[str] = None | ||
| awards: Optional[Awards] = None | ||
| imdb: Optional[Imbd] = None | ||
|
tmcneil-mdb marked this conversation as resolved.
Outdated
|
||
|
|
||
| model_config = { | ||
| "populate_by_name" : True | ||
| } | ||
|
|
||
|
|
||
| ''' | ||
| So this an interesting conversion. Pydanic doesn't cleanly support constructing | ||
| models that have MongoDB query operators as field names. This becomes an issue when | ||
| we want to convert the validated model back to a dictionary to use as a MongoDB query filter. | ||
| For example, if a user leaves the 'q' parameter blank, we don't want to include the '$text' operator | ||
| in the filter at all but validation will send an empty value for it and that causes errors. So | ||
| I am handling the validation in the query router itself, but leaving this here as an example of how | ||
| it could be done, if I am wrong about Pydantic's capabilities. | ||
| ''' | ||
|
|
||
| class TextFilter(BaseModel): | ||
| search: str = Field(..., alias="$search") | ||
|
|
||
| class RegexFilter(BaseModel): | ||
| regex: str = Field(..., alias="$regex") | ||
| options: Optional[str] = Field(None, alias="$options") | ||
|
|
||
| class RatingFilter(BaseModel): | ||
| gte: Optional[float] = Field(None, alias="$gte") | ||
| lte: Optional[float] = Field(None, alias="$lte") | ||
|
|
||
| class MovieFilter(BaseModel): | ||
| text: Optional[TextFilter] = Field(None, alias="$text") | ||
| genres: Optional[RegexFilter] = None | ||
| year: Optional[int] = None | ||
| imdb_rating: Optional[RatingFilter] = Field(None, alias="imdb.rating") | ||
|
|
||
| model_config = { | ||
| "populate_by_name" : True | ||
| } | ||
|
|
||
|
|
||
| class Pagination(BaseModel): | ||
| page: int | ||
| limit: int | ||
| total: int | ||
| pages: int | ||
|
|
||
|
|
||
| class CreateMovieRequest(BaseModel): | ||
| title: str | ||
| year: Optional[int] = None | ||
| plot: Optional[str] = None | ||
| fullplot: Optional[str] = None | ||
| genres: Optional[list[str]] = None | ||
| directors: Optional[list[str]] = None | ||
| writers: Optional[list[str]] = None | ||
| cast: Optional[list[str]] = None | ||
| countries: Optional[list[str]] = None | ||
| languages: Optional[list[str]] = None | ||
| rated: Optional[str] = None | ||
| runtime: Optional[int] = None | ||
| poster: Optional[str] = None | ||
|
|
||
|
|
||
|
|
||
| class SuccessResponse(BaseModel, Generic[T]): | ||
| success: bool = True | ||
| message: Optional[str] | ||
| data: T | ||
| timestamp: str | ||
| pagination: Optional[Pagination] = None | ||
|
|
||
|
|
||
| class ErrorDetails(BaseModel): | ||
| message: str | ||
| code: Optional[str] | ||
| details: Optional[Any] = None | ||
|
|
||
| class ErrorResponse(BaseModel): | ||
| success: bool = False | ||
| message: str | ||
| error: ErrorDetails | ||
| timestamp: str | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.