<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Building ExamReady]]></title><description><![CDATA[Notes from building ExamReady (examready.at), an exam prep platform for the German B1/B2 exams ÖIF, ÖSD and Goethe. Next.js, NestJS, MongoDB, AI grading, solo f]]></description><link>https://ladanovnick.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 14 Oct 2026 03:00:55 GMT</lastBuildDate><atom:link href="https://ladanovnick.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Versioned seed data: updating 20 exam tests in production without losing user progress]]></title><description><![CDATA[I'm building ExamReady, an independent exam prep platform for the German B1/B2 exams (ÖIF, ÖSD, Goethe). It is not affiliated with or endorsed by any of those institutions. It has 20 full model tests,]]></description><link>https://ladanovnick.hashnode.dev/versioned-seed-data-updating-20-exam-tests-in-production-without-losing-user-progress</link><guid isPermaLink="true">https://ladanovnick.hashnode.dev/versioned-seed-data-updating-20-exam-tests-in-production-without-losing-user-progress</guid><category><![CDATA[MongoDB]]></category><category><![CDATA[nestjs]]></category><category><![CDATA[Devops]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[Nikolay Ladanov]]></dc:creator><pubDate>Sun, 11 Oct 2026 22:05:49 GMT</pubDate><content:encoded><![CDATA[<p>I'm building <a href="https://examready.at">ExamReady</a>, an independent exam prep platform for the German B1/B2 exams (ÖIF, ÖSD, Goethe). It is not affiliated with or endorsed by any of those institutions. It has 20 full model tests, and I keep improving them: fixing a typo, adding an explanation, reworking a task.</p>
<p>The hard part is not editing the content. It is shipping those edits to production while real users have progress attached to it. This post is about how I update the exam content without losing anything the users did.</p>
<h2>The problem</h2>
<p>Exam content in my app is data in MongoDB: exams, model tests, modules, tasks and assets. Users produce their own data on top of it: accounts, attempts and submissions. Those two kinds of data live in the same database but have completely different lifecycles.</p>
<ul>
<li>Content is authored by me. It changes when I change the code.</li>
<li>User data is created at runtime. It must never be lost.</li>
</ul>
<p>If a content update wipes a user's attempts, or breaks the link between a user's progress and a model test, that is a bug I cannot take back. So the update mechanism has to be safe by construction.</p>
<h2>The design I chose</h2>
<p>The exam content lives in code, in the repository, as typed seed data. The database is a copy of it. That gives me version control, review and diffs for content, which I would not get from editing documents in a database console.</p>
<p>To keep the copy in sync I use one number, a <code>SEED_VERSION</code> in the backend. When the backend starts, it compares that number with the last applied version. If they differ, it reseeds. If they match, it does nothing.</p>
<p>A generic sketch:</p>
<pre><code class="language-ts">export const SEED_VERSION = 1; // bumped on structural content changes

const CONTENT_COLLECTIONS = ['exams', 'modelTests', 'modules', 'tasks', 'assets'] as const;

async function seedIfNeeded(db: Db) {
  const applied = await getAppliedSeedVersion(db); // from a small meta document
  if (applied === SEED_VERSION) return;

  for (const name of CONTENT_COLLECTIONS) {
    await reseedCollection(db, name);
  }
  await setAppliedSeedVersion(db, SEED_VERSION);
}
</code></pre>
<p>The key line is the list of collections. A reseed touches the content collections and nothing else. Users, attempts and submissions are not in that list, so they cannot be affected, even if I make a mistake in the seeding code.</p>
<p>Why not a proper migration framework? For a solo project whose content is already versioned in git, the seed version is the migration. There is no history of up and down scripts to maintain, because the target state is always "the content in this commit". The price is that I have to be strict about what a reseed is allowed to touch, and that is what the explicit collection list is for.</p>
<p>It also keeps the mental model small. At any moment I can answer two questions: which version of the content is in the code, and which version is in the database. If they differ, the next start of the backend closes the gap.</p>
<h2>Stable slugs keep progress linked</h2>
<p>Attempts and progress refer to content. If they pointed at database-generated ids, a reseed would create new ids and orphan every attempt.</p>
<p>So model tests have stable slugs, written by me, that never change once a test is published. Progress is linked through the slug, not through an id that the seed process generates:</p>
<pre><code class="language-ts">// pseudo-code: upsert by a stable key
await db.collection('modelTests').updateOne(
  { slug: test.slug },
  { $set: { ...test } },
  { upsert: true }
);
</code></pre>
<p>One more detail: because the seed is an upsert by slug, running it twice gives the same result as running it once. I like that property a lot. If a deploy is interrupted halfway, I can simply start the backend again.</p>
<p>With that, a reseed refreshes the content of "the same test" instead of creating a different one. The user's attempt still points at the same slug afterwards. The rule I follow: a slug is an identity, not a label. If I reword a title, the slug stays.</p>
<h2>Small fixes without a reseed</h2>
<p>Not every change deserves a version bump. A typo, a corrected explanation or a better hint is a small text change. For those I sync the changed text in place and leave <code>SEED_VERSION</code> alone. A reseed is for structural changes: new tasks, reordered modules, changed task types.</p>
<p>I keep that distinction on purpose. The smaller the operation, the less there is to go wrong, and fixing a typo should never feel like a migration.</p>
<h2>Before a production reseed: a backup</h2>
<p>Even with a narrow reseed, I take a database dump before I deploy a version bump. The dump is the answer to "what if I got something wrong in a way I did not expect". It is cheap, and my routine is:</p>
<ol>
<li>Dump the database.</li>
<li>Deploy the new version.</li>
<li>Check that the backend started and seeded.</li>
<li>Open a test or two and look at them.</li>
</ol>
<p>The dump also changes how I think about the deploy. When I know I can go back, I look at the result calmly instead of nervously. That alone is worth the extra minute.</p>
<h2>What went wrong</h2>
<p><strong>1. Treating the whole database as seed data.</strong> My first idea was simpler: wipe and reseed everything on a version change. It works on a laptop, where nobody has progress. It would have been a disaster in production. Splitting "content" from "user data" and naming the content collections explicitly was the most important correction.</p>
<p><strong>2. Using generated ids as references.</strong> An early version linked progress to ids produced during seeding. Changing the seed order changed the ids. Moving to stable slugs fixed it, but it showed me that identity has to be designed, not left to the database.</p>
<p><strong>3. Bumping the version for everything.</strong> For a while every tiny text fix meant a version bump and a full content reseed. It was safe, but heavy, and it made me hesitate to fix small things. In-place sync for small text changes removed that friction.</p>
<p><strong>4. Leaving the backup as a manual habit.</strong> The seeding code is safe by construction, but the backup depends on me remembering it. A habit is weaker than a script, and the obvious improvement is to make the dump a step of the deploy that cannot be skipped.</p>
<h2>Takeaways</h2>
<ul>
<li>Separate content from user data, in code and in the database.</li>
<li>Make the reseed's scope an explicit list of collections, so mistakes have a small blast radius.</li>
<li>Give published content a stable identity, and link user data to that identity.</li>
<li>Keep a cheap path for small fixes and an explicit path for structural changes.</li>
<li>Take a backup before every production reseed.</li>
</ul>
<p>I built most of this with AI coding agents (Claude Code), and the explicit list of collections turned out to be a useful guardrail when reviewing their changes: it is easy to see if a change tries to touch something it should not.</p>
<p>I'm building this at <a href="https://examready.at">examready.at</a>, feedback welcome.</p>
]]></content:encoded></item><item><title><![CDATA[Modeling exam content: typed tasks, separate answer keys and a scoring registry]]></title><description><![CDATA[I'm building ExamReady, a platform that helps people, mostly immigrants, prepare for official German exams: the ÖIF Integration Exam B1, ÖSD B1 (ZDÖ), ÖSD B2 and Goethe-Zertifikat B1. It's an independ]]></description><link>https://ladanovnick.hashnode.dev/modeling-exam-content-typed-tasks-separate-answer-keys-and-a-scoring-registry</link><guid isPermaLink="true">https://ladanovnick.hashnode.dev/modeling-exam-content-typed-tasks-separate-answer-keys-and-a-scoring-registry</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[MongoDB]]></category><category><![CDATA[architecture]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[Nikolay Ladanov]]></dc:creator><pubDate>Sun, 11 Oct 2026 21:58:09 GMT</pubDate><content:encoded><![CDATA[<p>I'm building <a href="https://examready.at">ExamReady</a>, a platform that helps people, mostly immigrants, prepare for official German exams: the ÖIF Integration Exam B1, ÖSD B1 (ZDÖ), ÖSD B2 and Goethe-Zertifikat B1. It's an independent project and is not affiliated with or endorsed by ÖSD, ÖIF or the Goethe-Institut.</p>
<p>I'm a solo developer, and the first thing I had to get right wasn't the UI. It was the shape of the data. This post is about how I modeled exam content, what I got wrong along the way, and what I would do again.</p>
<h2>The problem</h2>
<p>A language exam looks like a list of questions. It isn't.</p>
<p>A full model test (a "Modellsatz") has four modules: reading (Lesen), listening (Hören), writing (Schreiben) and speaking (Sprechen). Each module has several tasks, and every task has its own format: true/false statements about a text, matching ads to situations, multiple choice, a listening task where the audio plays twice, writing an e-mail, speaking prompts. Tasks also come with assets such as audio files and images.</p>
<p>On top of that I had a few requirements:</p>
<ul>
<li>Tasks should look like the real exam, with the real structure.</li>
<li>Objective tasks should be scored automatically and consistently.</li>
<li>The same tasks should work inside short lessons, not only in full tests.</li>
<li>The correct answers must never reach the browser before the user submits.</li>
</ul>
<h2>The design I chose</h2>
<p>The hierarchy is plain and, I think, the natural one for exams:</p>
<pre><code>Exam &gt; Model test &gt; Module &gt; Task &gt; Asset
</code></pre>
<p>A model test belongs to an exam, a module belongs to a model test, a task belongs to a module, and assets (audio, images) hang off tasks. Each level is its own MongoDB collection and references its parent by id. I did consider embedding everything in one big document per model test. I decided against it because tasks get reused in lessons and I wanted to load one task without loading a whole test.</p>
<p>The most important decision was that <strong>every task has a <code>type</code></strong>, and the type decides everything else: the shape of the content, which component renders it, and how it is scored. A discriminated union in TypeScript makes this explicit:</p>
<pre><code class="language-ts">type TaskContent =
  | { type: 'true_false'; text: string; statements: { id: string; text: string }[] }
  | { type: 'matching'; situations: Item[]; ads: Item[] }
  | { type: 'multiple_choice'; question: string; options: Item[] }
  | { type: 'listening_two_plays'; audioAssetId: string; questions: Item[] }
  | { type: 'email_writing'; prompt: string; points: string[] }
  | { type: 'speaking'; prompt: string };

type TaskType = TaskContent['type'];

interface Task {
  slug: string;
  moduleId: string;
  content: TaskContent;
}
</code></pre>
<p>The frontend switches on <code>content.type</code> to pick a renderer, and TypeScript narrows the rest. Adding a new task type is a compile error everywhere I forgot to handle it, which is exactly what I want.</p>
<h2>Content and answer key are separate</h2>
<p>The first rule I set was that the correct answer is never stored inside the task. Tasks live in one collection, answer keys live in another, linked by task id:</p>
<pre><code class="language-ts">interface AnswerKey {
  taskId: string;
  type: TaskType;
  answers: Record&lt;string, string&gt;; // item id -&gt; correct option id
  explanations?: Record&lt;string, string&gt;;
}
</code></pre>
<p>The flow is then simple. The client asks for a task and receives only the content. The user answers and submits. The server loads the key, scores the answer, and only in the response does the key (and the explanation) travel to the browser.</p>
<p>There is no "strip the answers before sending" step to forget. The endpoint that serves tasks does not query the key collection at all. If I make a mistake in a serializer, the worst outcome is a missing field, not a leaked answer.</p>
<h2>A scoring registry</h2>
<p>Each task type has a scoring function, and a registry maps the type to that function:</p>
<pre><code class="language-ts">type Scorer = (key: AnswerKey, answer: unknown) =&gt; { points: number; max: number };

const scorers: Record&lt;TaskType, Scorer&gt; = {
  true_false: scoreTrueFalse,
  matching: scoreMatching,
  multiple_choice: scoreMultipleChoice,
  listening_two_plays: scoreListening,
  email_writing: scoreWriting,
  speaking: scoreSpeaking,
};

export function scoreTask(task: Task, key: AnswerKey, answer: unknown) {
  return scorers[task.content.type](key, answer);
}
</code></pre>
<p>This is a generic sketch, not my real code, but the idea is the same. Because the registry is typed as <code>Record&lt;TaskType, Scorer&gt;</code>, TypeScript refuses to compile if I add a task type without a scorer.</p>
<p>The part I like most: <strong>the same function scores full tests and mini tasks inside lessons.</strong> A lesson that ends with a true/false task calls <code>scoreTask</code>, and so does a full model test. There is one place where "what counts as correct" is defined.</p>
<h2>What went wrong</h2>
<p>I made three mistakes worth sharing.</p>
<p><strong>1. Keeping the key next to the content.</strong> My first instinct was a <code>correct: true</code> flag on each option. It is convenient, and it is exactly how answers leak. Every endpoint then needs to remember to remove those flags. I moved the keys out before it became a habit, but it was a rewrite I could have avoided.</p>
<p><strong>2. Stringly typed task types.</strong> Early on, a task type was just a string, checked in a few <code>if</code> statements in different places. Adding a type meant hunting for every place that cared. The union plus the <code>Record</code> registry fixed that, but it came after I had already spread the logic around.</p>
<p><strong>3. Thinking of scoring as right or wrong.</strong> Multiple choice fits a boolean nicely. A matching task with several pairs, or a writing task, does not. Returning points and a maximum, instead of a boolean, made the registry usable for all types without special cases. I'm glad I changed this before building the results screens on top of it.</p>
<h2>Why separate collections and not one big document</h2>
<p>MongoDB makes it tempting to put a whole model test into one document: modules, tasks and all. It reads nicely in a console. But my tasks have a second life as mini tasks inside lessons, and a lesson should be able to load exactly one task without dragging a full test along. References between collections cost a little more at query time and buy a lot of flexibility.</p>
<p>Assets follow the same logic. Audio and images are referenced by id from the task, so one file can be reused and replaced without touching the task content.</p>
<h2>Takeaways</h2>
<ul>
<li>Model the real hierarchy of the thing you are teaching. For exams that is exam, test, module, task, asset.</li>
<li>Give every task a type and let the type drive rendering and scoring.</li>
<li>Store content and answer keys separately, so the safe path is the default path.</li>
<li>Use one scoring registry for every context. Lessons and full tests should never disagree about what is correct.</li>
<li>Let the compiler tell you what you forgot. A typed union and a <code>Record</code> over it are cheap and effective.</li>
</ul>
<p>None of this is clever. It is mostly about deciding early where the boundaries are. I built this as a solo developer with a lot of help from AI coding agents (Claude Code), and having clear types and boundaries made it much easier to review what the agents wrote.</p>
<p>In the next posts I'll write about the lesson player, the hosting setup and how I update exam content in production without losing user progress.</p>
<p>I'm building this at <a href="https://examready.at">examready.at</a>, feedback welcome.</p>
]]></content:encoded></item><item><title><![CDATA[Best websites 2024 to learn SQL for free]]></title><description><![CDATA[Curious about managing and querying databases like an expert without spending any money? Explore the best websites to learn SQL for free and equip yourself with the skills and knowledge needed to master this essential database language at no cost.
Ov...]]></description><link>https://ladanovnick.hashnode.dev/best-websites-2024-to-learn-sql-for-free</link><guid isPermaLink="true">https://ladanovnick.hashnode.dev/best-websites-2024-to-learn-sql-for-free</guid><category><![CDATA[SQL]]></category><category><![CDATA[MySQL]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[education]]></category><dc:creator><![CDATA[Nikolay Ladanov]]></dc:creator><pubDate>Sat, 20 Jul 2024 18:28:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1721426409105/cb047101-52f6-4ef0-a9ff-c1c51881f631.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Curious about managing and querying databases like an expert without spending any money? Explore the best websites to learn SQL for free and equip yourself with the skills and knowledge needed to master this essential database language at no cost.</p>
<h2 id="heading-overview-top-7-websites">Overview: top 7 websites</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Name</td><td>Rating</td><td><strong>Website Link</strong></td></tr>
</thead>
<tbody>
<tr>
<td>SQL Academy</td><td>⭐️ ⭐️ ⭐️ ⭐️ ⭐️</td><td><a target="_blank" href="https://sql-academy.org/en">The website link</a></td></tr>
<tr>
<td>Codecademy</td><td>⭐️ ⭐️ ⭐️ ⭐️ ⭐️</td><td><a target="_blank" href="https://www.codecademy.com/courses/learn-sql/">The website link</a></td></tr>
<tr>
<td>Udacity</td><td>⭐️ ⭐️ ⭐️ ⭐️</td><td><a target="_blank" href="https://www.udacity.com/course/intro-to-relational-databases--ud197">The website link</a></td></tr>
<tr>
<td>SQL Bold</td><td>⭐️ ⭐️ ⭐️ ⭐️</td><td><a target="_blank" href="https://sqlbolt.com/">The website link</a></td></tr>
<tr>
<td>Datacamp</td><td>⭐️ ⭐️ ⭐️ ⭐️</td><td><a target="_blank" href="https://www.datacamp.com/tracks/sql-fundamentals">The website link</a></td></tr>
<tr>
<td>Khanacademy</td><td>⭐️ ⭐️ ⭐️</td><td><a target="_blank" href="https://www.khanacademy.org/computing/computer-programming/sql">The website link</a></td></tr>
<tr>
<td>SqlZoo</td><td>⭐️ ⭐️</td><td><a target="_blank" href="https://sqlzoo.net/wiki/SQL_Tutorial">The website link</a></td></tr>
</tbody>
</table>
</div><h3 id="heading-sql-academy">SQL Academy</h3>
<p>SQL Academy offers a free, beginner-friendly interactive SQL course that clearly explains how to construct SQL queries. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721427256084/b87148fb-a837-4279-82dd-d9b8823abe11.png" alt class="image--center mx-auto" /></p>
<p>After each topic, the site provides exercises to help reinforce the material covered. Additionally, the site features over 100 ranked challenges, organized by difficulty, to practice SQL query writing skills.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721427542370/9277f634-ac38-4c71-be0d-e65193590570.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-codecademy"><strong>Codecademy</strong></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721475517784/9a5a8a13-37d0-439c-bda4-d11a51950e9a.png" alt class="image--center mx-auto" /></p>
<p>Codecademy provides an interactive SQL course aimed at beginners, offering a hands-on approach to learning SQL. The “Learn SQL” course is structured to teach the fundamentals of SQL, essential for managing large datasets and analyzing real data. With no prerequisites required, the course is accessible to anyone interested in data science, analytics, or engineering. It includes four lessons, five projects, and four quizzes, totaling approximately 5 hours of content.</p>
<h2 id="heading-udacity-intro-to-relational-databases">Udacity “Intro to Relational Databases”</h2>
<p>Udacity’s “Intro to Relational Databases” course is a comprehensive video-based SQL tutorial. It covers a range of beginner to intermediate topics, preparing students for various SQL applications.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721428374566/72675c6b-b1eb-45c7-ac76-3d6603e87807.png" alt class="image--center mx-auto" /></p>
<p>The course includes coding exercises, but students need to execute these in their own runtime environments as interactive coding tests are not provided. This course is ideal for visual learners.</p>
<h2 id="heading-sql-bold">SQL Bold</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721476305007/dd44f85e-6f43-47ca-b893-ff0d8e2ee2bd.png" alt class="image--center mx-auto" /></p>
<p>SQLBolt is an interactive online platform focused on teaching SQL (Structured Query Language) through a series of lessons and exercises. It aims to help both technical and non-technical users quickly and practically learn SQL directly in their browser. The platform covers the essentials of querying, manipulating, and transforming data from a relational database, providing a foundational understanding of relational databases and SQL syntax.</p>
<p>SQLBolt’s tutorials are designed to start with basic concepts and gradually introduce more complex queries and operations. This step-by-step approach ensures that learners build a strong foundation before tackling advanced topics. Each lesson ends with interactive exercises, allowing learners to immediately apply what they’ve learned in a practical, real-time environment.</p>
<h2 id="heading-datacamp">Datacamp</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721489101548/905feed1-e523-43a5-ad10-5f3c93f56ec8.png" alt class="image--center mx-auto" /></p>
<p>DataCamp’s “SQL Fundamentals” track is a comprehensive learning path crafted to teach essential SQL skills. It consists of seven courses and one project, amounting to roughly 26 hours of content. The track starts with SQL basics, such as structuring and querying relational databases, and advances to more complex topics like summarizing and joining tables, utilizing window functions, and leveraging built-in PostgreSQL functions for data analysis.</p>
<h2 id="heading-khanacademy">Khanacademy</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721491363557/409a2593-e406-436d-ac0f-57f2c593a5fa.png" alt class="image--center mx-auto" /></p>
<p>Khan Academy’s “Intro to SQL: Querying and Managing Data” offers a blend of video tutorials and interactive coding challenges, providing a comprehensive learning experience. The video lessons are presented at the command-line level, clearly illustrating concepts through the code itself. Frequent coding challenges accompany these lessons, reinforcing the material with a variety of problem sets.</p>
<p>This course is ideal for learners who prefer a combination of visual instruction and hands-on coding exercises.</p>
<h2 id="heading-sqlzoo">SqlZoo</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721491565624/3f3cdeab-b76a-4138-a6a5-119a7e6c74f0.png" alt class="image--center mx-auto" /></p>
<p>SQLZoo is an interactive, Wiki-based tutorial designed for beginners learning SQL. It features lessons and projects that start with basic functions like COUNT, SUM, and AVG, and progress to intermediate projects.</p>
<p>The lessons focus on interactive coding challenges that are self-explanatory, making it an excellent resource for students who prefer learning SQL through small, straightforward exercises.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Whether you are a beginner or looking to brush up on your SQL skills, these free resources provide comprehensive and practical learning experiences. Start your journey to mastering SQL today with these top-rated websites.</p>
]]></content:encoded></item></channel></rss>