“Did We Already Watch This?” — Building KindaSeen with FastAPI and Next.js

A few months ago, my friends and I kept running into the same question whenever we talked about movies, dramas, anime, or variety shows:
“Did we already watch this before?”
Sometimes we remembered the title but forgot whether we had finished it. Other times, we completely forgot we had already seen it at all.
That simple problem inspired me to build KindaSeen, a full-stack personal media repository designed to help users track and organize the media they’ve consumed in one centralized platform.
The goal of the project was not only to create a useful application, but also to gain hands-on experience building a real-world full-stack system with modern web technologies.
What KindaSeen Currently Supports
- User authentication with Supabase
- CRUD operations for personal media records
- TMDB-powered search functionality
- Watchlist system
- Favorites system
- Persistent PostgreSQL storage
- Dockerized backend deployment
- Separate frontend/backend deployment workflow
Tech Stack
Frontend
- Next.js
- React
- Tailwind CSS
- Shadcn/ui
- Vercel deployment
Backend
- FastAPI
- PostgreSQL
- Docker
- Render deployment
External Services
- Supabase Authentication
- TMDB API integration
One of the main goals of this project was to simulate a more realistic production workflow by using a decoupled frontend/backend architecture instead of building everything inside a single monolithic application.
In this article, I’ll share:
- Why I chose this architecture
- How I integrated TMDB into the application
- Challenges I faced during deployment
- What I Learned From Building KindaSeen
Why I Chose This Architecture
Instead of building a monolith using Next.js API routes, I decided to decouple the application into a Next.js frontend and a FastAPI backend. This decision was driven by three main factors:
- AI Compatibility & Future Proofing: While researching the job market, I noticed that most companies building AI products heavily rely on Python. By choosing FastAPI now, the project is ready to seamlessly integrate Python-based AI models or recommendation algorithms later.
- High Performance & Rapid Development: FastAPI is incredibly fast to write and run. It allowed me to build robust backend logic with minimal boilerplate code, keeping the development momentum high.
- Out-of-the-Box Interactive Docs (Swagger UI): One of my favorite features is the automatic Swagger documentation. It made debugging and testing APIs an absolute breeze, acting as a bridge that allowed me to easily verify my endpoints before connecting them to the Next.js frontend.
How I Integrated TMDB into KindaSeen
1. Backend: Fetching from TMDB
I created a dedicated tmdb module in my FastAPI app with a single search endpoint:
# app/tmdb/router.py
@router.get("/search", response_model=list[dict])
async def search(
q: str = Query(..., min_length=1),
media_type: str | None = Query(None, pattern="^(movie|tv)$"),
_user_id=Depends(get_current_user_id),
):
results = await search_tmdb(q, media_type)
return [r.__dict__ for r in results]
The service layer calls TMDB's /search/multi endpoint using httpx, and normalizes the response into a clean dataclass:
@dataclass
class TMDBResult:
tmdb_id: int
title: str
media_type: str
poster_url: str | None
overview: str
tmdb_rating: float | None
genres: list[str]
release_year: int | None
One thing worth noting: TMDB has its own media_type (movie | tv), which doesn't map 1:1 to my app's types (movie | drama | anime | variety | ...). I kept them separate — TMDB's type is only used for search display, while the user always picks their own media type when saving a record.
2. Storing Metadata
Rather than only storing an external_id and fetching metadata on demand, I chose to persist the full TMDB metadata alongside each record:
# records/model.py
tmdb_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
poster_url: Mapped[str | None] = mapped_column(Text, nullable=True)
overview: Mapped[str | None] = mapped_column(Text, nullable=True)
tmdb_rating: Mapped[float | None] = mapped_column(Numeric(3, 1), nullable=True)
genres: Mapped[list[str] | None] = mapped_column(ARRAY(String), nullable=True)
release_year: Mapped[int | None] = mapped_column(Integer, nullable=True)
This makes the data self-contained and query-ready — especially useful when I add AI-powered recommendations later.
3. Frontend: Search Entry Point
On the frontend, I built the way to search:
-
Header — a search dialog always visible when logged in. Selecting a result navigates to
/recordswith TMDB data passed as URL query params, which then auto-opens the Add Record dialog with the title pre-filled.
In the case, the user still chooses the media type and status themselves — TMDB only fills in the title, year, overview, and rating.
Challenges I Faced During Deployment
Since I had prior experience with Docker and Render, the overall containerization and deployment workflow felt familiar and manageable. Because KindaSeen is currently a personal, side project, I decided to stick with a lightweight setup rather than jumping into heavy cloud infrastructure like AWS or GCP.
However, deploying a split frontend/backend architecture on free-tier hosting brought one major headache: Render's Free Tier Cold Start.
If the backend server hasn't received any traffic for a while, Render spins down the container. The next request can take up to 50 seconds or more to respond—a terrible user experience for anyone opening the app.
To bypass this limitation, I implemented a simple yet effective Keep-Alive workaround:
-
Created a
/healthEndpoint: I added a lightweight route in FastAPI that simply returns a200 OKstatus. -
Configured UptimeRobot: I set up a free UptimeRobot monitor to ping this
/healthendpoint every few minutes.
This constant heartbeat keeps the Render container awake, ensuring the backend is always warm and ready to respond immediately when a user visits the app!
What I Learned From Building KindaSeen
While this isn't my very first time deploying a full-stack application, building KindaSeen allowed me to level up my development workflow and explore modern software design patterns.
Here are the key takeaways from this project:
- Mastering a Feature-Based Architecture: Instead of shoving all routers and models into a single place, I organized my FastAPI backend using a feature-based directory structure (grouping by features like favorites, tmdb, and records). This made the codebase significantly more modular, maintainable, and easier to scale.
- Leveraging Swagger UI for Seamless Testing: I learned how to fully utilize FastAPI's automatic Swagger documentation. It completely changed how I debugged my backend, allowing me to test edge cases visually before writing any frontend fetch requests.
- Accelerating Development with Supabase (BaaS): Building a secure, robust authentication system from scratch can take days. By integrating Supabase Auth, I learned how to offload the heavy lifting of user management safely, which heavily accelerated my development speed and allowed me to focus on the core features of KindaSeen.
What's Next? 🚀
KindaSeen is just getting started. Now that the core infrastructure is solid, my next goals are:
- AI-Powered Recommendations: Leveraging the stored TMDB metadata to suggest what users should watch next.
- Social Sharing: Allowing friends to see each other's watchlists (to finally settle the "what should we watch tonight" debate).
- Statistics Dashboard: Visualizing media consumption habits over time.
Thanks for reading! 🙌
If you've ever struggled to keep track of your media or if you're building something similar with FastAPI and Next.js, I'd love to connect!
- 🌐 Live Demo: KindaSeen
- 💻 Source Code: GitHub Repository
💬 Let's Chat!
How do you usually track your movies, dramas, or anime? Do you use a dedicated app, Notion, or just a classic spreadsheet?
Also, if you have any feedback on my tech stack or deployment workaround, feel free to drop it in the comments below! 👇
Top comments (0)