← All Projects
Completed, Feb 2025

Filmterest

This is a project built to learn React and Node from scratch as well as full-stack development. The project is a social app that suggests and matches users by Letterboxd movie reviews.

React Node.js MongoDB Full-Stack
View on GitHub →

No AI was used to build this project.

Going into this project, I'd never really built a web app before this, so I picked a project idea I'd actually want to use myself as an excuse to learn properly. I made a social app that matches people by film taste using Letterboxd, since I already log everything I watch there. This was a really useful project that taught me REST APIs, DB design, auth, and OAuth.

In this app, users log posts and reviews, see a feed of friends' recent Letterboxd activity, and can check a compatibility score against any Letterboxd username, comparing their own diary against someone else's. Under the hood it's a fairly standard full-stack shape, with React and Redux on the frontend, an Express API, and MongoDB via Mongoose. A couple of the smaller decisions ended up being the parts I learned the most from.

Filmterest is a fairly standard full-stack shape, a React and Redux frontend, an Express API, and MongoDB through Mongoose. Auth supports both an email and password flow that issues a JWT I sign myself, and Google Sign-In, which hands back Google's own ID token instead. The Letterboxd integration sits behind a small client wrapper that either calls the real API or falls back to a deterministic mock data layer, depending on whether credentials are configured.

A request for Letterboxd data branches on whether an API key is configured, either to the mock data layer seeded from a hash of the username, or to the real Letterboxd API, both going through the same client wrapper

This was also my first time designing REST endpoints and working across an entire stack instead of just one piece of it. I settled on keeping resource identifiers and filters in the URL and reserving the request body for anything that actually changes state, which kept the API predictable once I stuck to it consistently. To keep the frontend's Redux state from drifting out of sync with the server, I stopped trusting optimistic local updates and instead updated the store directly from whatever the response actually returned. Validation ended up living in both places on purpose, a lightweight check on the frontend for instant feedback, and the real enforcement on the server since that's the only copy I could actually trust. Bugs stopped being contained to one file the way they were in smaller class projects, and a mismatch between what the API returned and what the frontend expected could look like a rendering bug on one end and a data bug on the other. I also got a lot better at tracing those once I made a habit of checking the network tab's request and response together before touching any code.

The Letterboxd API requires application approval, and mine was still pending while I was building this, so I wrote a mock data layer that generates a deterministic fake "diary" for any username instead of stalling the project. It's seeded from an FNV-1a hash of the username through a small seedable PRNG called mulberry32, so the same username always gets the same fake watch history and match score instead of random noise on every request. Every mocked response is flagged with pending: true so the UI can show a banner about it, and the real controller checks for an API key at request time, so once Letterboxd approves access it starts returning live data with no code changes on either side. Building against an API I didn't have yet made me think harder about the interface between "real" and "mock" data than I would have otherwise.

I also didn't want the compatibility score to just count shared movies, since two people who've both seen a hundred popular films but rate everything oppositely shouldn't score as highly as two people with a smaller, pickier overlap who actually agree. So the score ended up half overlap, shared films over the union of both diaries, and half rating agreement, based on the average difference in their ratings on the films they've both logged. It took a couple of tries to land on weights that felt right when I tested it against my own account and a few friends'.

React Redux Express MongoDB JWT Google Sign-In Letterboxd API