Evolving a private language-learning platform from Node.js to FastAPI
A private vocabulary application for saving words in context and practicing them through configurable spaced repetition. Built and redesigned since 2019, it now uses a Python/FastAPI backend and React client. This case study follows the product constraints, data-model decisions, and architectural trade-offs behind that evolution.
Active · Portfolio ReleaseProject Snapshot
| Product | Private vocabulary and spaced-repetition application |
|---|---|
| Role | Sole designer and full-stack developer |
| Timeline | 2019 – present · current Python version started in 2026 |
| Current stack | FastAPI, SQLAlchemy 2.0 (async), Alembic, PostgreSQL, Redis · React 19, TypeScript, TanStack Query |
| Previous stacks | v1: Node.js/Express REST, MongoDB, React · v2: TypeScript GraphQL monorepo, MongoDB · Next.js/Prisma/PostgreSQL experiment |
| Demo policy | Public demo runs the same code against the live Merriam-Webster API, with a small search allowance per account. Demo accounts and their application records are deleted after 24 hours. The personal deployment stays private, with signup closed. |
Why This Project Exists
The Language Learning Platform began as a personal response to limitations I repeatedly encountered in vocabulary apps. I wanted a private space for learning: no public profile, social feed, rankings, shared editing, or pressure to study someone else's word list. Vocabulary is personal—what a learner captures, why it matters, and the context in which it was encountered are part of the learning record.
I originally learned English primarily through textbooks, with limited access to online media. The app's small game-like exercises, constrained visual treatment, examples, and images deliberately preserve the focused feeling of working through a good language textbook rather than recreating a crowded, gamified social platform.
Stable Product Principles
These principles have held across every version, v1 to v3. Each one carries a technical consequence that later decisions refer back to.
- P01
Private, learner-owned vocabulary
ProductA learner’s words, review history, and study patterns are visible only to them, and only they can edit them.
TechnicalEvery query is scoped to the signed-in user; another learner’s word or game session is reported exactly like one that does not exist.
- P02
Capture words in context
ProductWords come from real encounters and keep the learner’s own examples, translations, and image.
TechnicalEach learner keeps a personal copy of a dictionary entry, with editable definitions, examples, and images, alongside fully custom words.
- P03
Deliberate recall practice
ProductSpelling is a core learning outcome, hearing a word supports recall, and words come back on a schedule the learner controls.
TechnicalExplicit answer-comparison rules, audio that plays automatically unless it would give the answer away, and spaced repetition per word and per game, with presets.
- P04
Focused interaction
ProductThe interface supports concentration rather than competing for attention.
TechnicalPredictable game layouts, restrained visual effects, and accessibility-minded presentation.
How the Platform Evolved
One product domain, rebuilt as the stack I wanted to learn changed. Each version kept the principles above and tested a different architecture.
- V12019 – 2020Historical
REST API and React client
An English-only vocabulary trainer: Merriam-Webster lookups, personal word cards, and four practice games behind JWT authentication.
ExperimentBuilding the first complete web product and REST APINode.jsExpressMongoDBReactWhy it moved onWanted stronger typing, generated contracts, GraphQL, testing, and reusable UI.
- V22022 – 2026Historical
TypeScript GraphQL monorepo
Added Spanish, four more games (eight in total), custom words with examples and images, and a first simplified spaced-repetition schedule. Client, server, and a Storybook design system shared one GraphQL schema.
ExperimentShared types, GraphQL, monorepo tooling, tests, CI/CDNode.jsGraphQLReactTypeScriptMongoDBStorybookCypressDockerGitHub ActionsLernaWhy it moved onWanted PostgreSQL, Prisma, Redis, Next.js, and external-service integration.
- EXPERIMENT2025Abandoned
Next.js full-stack rewrite
A move to relational data inside a single Next.js app. Never published, and no longer maintained once work switched to the current stack.
ExperimentRelational data, caching, generated audio, rate limiting, image optimizationNext.jsTypeScriptPostgreSQLPrismaRedisWhy it moved onWanted deeper Python backend experience, separate client and server deployments, and explicit control over session handling and backend service boundaries.
- V32026 – presentCurrent
FastAPI backend and React client
Separate repositories joined by REST contracts: server-side sessions and configurable spaced repetition. The same build runs as a private deployment and a public demo, on Cloud Run and Netlify. Six games so far: all four from v1 and two from v2, with the remaining two from v2 being reworked.
ExperimentPython backend development, sessions, REST contracts, replaceable adaptersFastAPIPostgreSQLSQLAlchemyRedisReact
Modeling Personal Vocabulary
The app treats a word as a personal learning object, not a row in a shared dictionary. Vocabulary is private, contextual, precise, and owned by the learner, so the data model starts from one question:
v1 and v2 used MongoDB, where a document per word made it easy to keep adding fields as the product grew. In PostgreSQL, the word is split on purpose: the personal copy, its current schedule, and its review history each have their own lifetime and their own rules.
Where each piece lives
users ├── words personal copy · MW uuid links the source, null = custom word │ ├── word_examples editable list │ ├── word_progress current schedule · one row per game type │ └── review_logs one row per answer · append-only └── game_settings the learner's spaced-repetition settings games, game_languages shared catalogue Redis: game sessions temporary · deleted 20 minutes after the last answer
Logical ownership and relationships, not physical table nesting.
Why copy instead of reference
- The learner can edit their words freely
- A dictionary entry changing or disappearing can't change a saved word
- Reviewing already-saved vocabulary does not require another dictionary lookup
This duplicates dictionary-derived content, but lets saved vocabulary remain editable and independent of later provider changes. Personal context lives in the learner's copy: editable definitions, examples with translations, and their own image and caption.
How These Boundaries Are Verified
Each rule above is checked by an automated API test in the v3 backend. The tests call the app over HTTP against real PostgreSQL and Redis, and run in CI on every push.
- A learner cannot read or modify another learner’s vocabularyA second account gets 404 when editing or deleting the first account’s word and sees an empty vocabulary; the owner’s word is unchanged. Game sessions are checked the same way.test_other_users_words_are_invisible →test_other_user_cannot_use_a_session →
- An answer updates the scheduling state and appends a review recordAnswering a question saves word_progress for that game type and exactly one review_logs row, before the response is returned.test_answer_is_graded_and_progress_saved →
- Deleting an account removes everything it ownsThe demo’s cleanup relies on ON DELETE CASCADE: deleting a user through SQL or the ORM removes their words, examples, progress, review history, and settings, while another user’s data survives.test_sql_delete_removes_everything_the_user_owns →test_orm_delete_removes_everything_the_user_owns →
System at a Glance
Current — reflects the v3 Python/FastAPI backend as of 2026.
The React client talks to FastAPI over HTTPS with a session cookie. FastAPI keeps users, words, and learning progress in PostgreSQL, auth and game sessions in Redis, and looks words up in the Merriam-Webster API. The backend is the source of truth for domain state and cross-client configuration.
The client and server have separate build and deployment pipelines and communicate through a documented REST API. Changes still require coordination when the API contract changes.
Key Engineering Decisions
Shaping API responses around the client
Implementation Highlights
Capture words in context (P02) end to end: a word met in a book stays tied to that moment, from editing it to practicing it.
- STEP 01
Capture the encounter
Selected dictionary data and the learner’s own example become one learner-owned record: a personal copy, linked to its source by the Merriam-Webster id.
- STEP 02
Keep it with the word
Definitions, examples, audio, image, and caption stay editable on the learner’s copy, independent of later changes at the provider.
- STEP 03
Recall it in practice
The feedback screen reads the example from the saved copy, reconnecting the answer with where the learner first met the word.
Behind the games
Settings change how the next review is scheduled: a change applies to each word the next time it is answered, while reviews already scheduled keep their dates.