The problem
Every time I wanted to learn something new on YouTube — Excel, guitar, a new language — I hit the same wall: hundreds of videos on the topic, a lot of them near-duplicates, no real order, and no idea how long it would actually take to go from zero to competent.
So I built Cursos en Video: you type a topic, and it builds you a structured learning path entirely from existing YouTube videos — searched, filtered, ordered by prerequisite, deduplicated, with a time estimate for the whole thing.
The stack
Nothing exotic on purpose: Next.js 16 (App Router) + TypeScript + Tailwind, Supabase (Postgres + Auth + RLS), OpenAI Structured Outputs (strict: true) to generate the curriculum, and the YouTube Data API v3 (direct REST, no SDK) to search and pull video metadata.
The core flow: /api/generate-path calls the YouTube Data API to gather candidate videos for a topic, filters them, and hands them to OpenAI to order into modules by prerequisite. YouTube's default quota is 10,000 units/day, and a single search.list call costs 100 — so every request is rate-limited (3/minute, 15/day per user), enforced with an atomic Postgres function rather than in application memory, and it fails open (with a Sentry alert) if the limiter itself breaks — better to occasionally let one extra request through than to take the whole app down over a rate-limiter bug.
Here are three things I ran into that I think are worth sharing.
1. Never let an LLM invent an identifier
The app also recommends affiliate resources (mostly books) related to a topic. Early on I asked OpenAI to suggest books and generate the Amazon product link. Bad idea: LLMs will happily hallucinate an ASIN that points to the wrong product, or to nothing at all.
The fix was to change what I was asking the model to do. Now OpenAI only returns real, well-known books (title + author + why it fits) — and is never asked for an ASIN or a URL. A separate, fully deterministic function builds an Amazon search-results link for "title + author" with my affiliate tag. It still earns commission without depending on the model getting the exact product right.
General lesson: if a piece of data has to be correct, don't let the model generate it — let it generate the inputs to a deterministic lookup instead.
2. Concurrency bugs show up exactly where you don't expect them
There's a daily cron that emails newsletter subscribers about newly-approved affiliate resources for topics they follow. I later added a second trigger: notify subscribers the moment an admin approves a resource, instead of waiting for the next day's cron.
Two triggers reading and writing the same table sounds harmless — until two approvals land seconds apart. Both would read the same "not yet notified" resources before either marked them as sent, and both would happily email the same subscribers twice.
The fix: claim each resource atomically by setting a notified_at timestamp in a single UPDATE ... WHERE notified_at IS NULL, before sending anything, relying on Postgres's row locking. A second overlapping run's WHERE clause stops matching the instant the first run's update commits — no explicit lock needed. If sending fails outright, the claim is released (notified_at = null) so the next run retries it, instead of silently losing it.
3. Sometimes the boring option is the correct one
I needed a way to export a learning path to PDF. The obvious modern answer is a headless-browser PDF service — Puppeteer, or a hosted API. On a serverless deploy that means cold starts, timeout risk, and a new dependency to keep alive.
Instead: a plain read-only page that reuses the same curriculum rendering already built for the public share links, with print-specific CSS, and the browser's own "Print → Save as PDF." Zero new dependencies, zero server-side rendering cost, zero timeout risk — and it looks exactly like what the user already sees on screen.
What's next
Multi-language support is next on the list, along with a few more affiliate networks. If you're curious, Cursos en Video is free, no signup needed to try it — currently Spanish-only, but I'd genuinely like to know if there's interest in an English version. Building this solo, so feedback (harsh included) is very welcome.
Top comments (0)