Stop 19 · Build Full-Stack Portfolio ProjectsProjects · 7/12

Learning Management System

Part of the Full Stack Developer Roadmap

In short

You're using one right now. Courses, lessons, progress ticks and quizzes look simple, but tracking progress per user, per lesson, without slowing the page, is a real design problem.

A course platform with lessons, progress tracking and quizzes — directly relevant given the roadmap tool you're using right now follows a similar UX pattern.

The purple blob as a tuition teacher ticks a big register beside a stack of notebooks.

Think of it like this

The tuition teacher's register

Sir keeps one register: a row per student, a column per chapter, a tick when you finish. A glance shows you're 60% done. Your LMS is that register, with the database keeping the ticks.

Watch it happen

Marking a lesson done

Browser
API
Database

POST /lessons/31/done

1/5The student clicks 'Mark as done' at the end of a lesson.

The big ideas

What you need to know

Course, module, lesson

A clear hierarchy with an order column at each level, so you can reorder lessons without renaming them.

Progress is a join table

lesson_progress(user_id, lesson_id) with both as the primary key. Done is simply 'row exists'.

Quizzes checked on the server

Send questions without answers. The server grades the attempt and stores the score.

Video from storage

Store videos in object storage or a video service, and serve signed URLs to enrolled students only.

See it in code

Walk through the code

sql
1CREATE TABLE lesson_progress (2  user_id      bigint REFERENCES users(id),3  lesson_id    bigint REFERENCES lessons(id),4  completed_at timestamptz NOT NULL DEFAULT now(),5  PRIMARY KEY (user_id, lesson_id)6);7 8-- Mark done (safe to call twice)9INSERT INTO lesson_progress (user_id, lesson_id) VALUES ($1, $2)10ON CONFLICT DO NOTHING;11 12-- Course progress for the course page13SELECT round(100.0 * count(lp.lesson_id) / nullif(count(l.id), 0)) AS percent14FROM lessons l15LEFT JOIN lesson_progress lp ON lp.lesson_id = l.id AND lp.user_id = $116WHERE l.course_id = $2;

1/3The pair is the primary key, so each student can complete each lesson only once.

Progress is just rows in a join table. One idempotent insert marks a lesson done, and one LEFT JOIN works out the percentage.

Save yourself the pain

Common mistakes

Oops

Sending quiz answers to the browser and grading in React.

Do this instead

Keep answers on the server. The browser sends choices, the server returns the score.

Oops

Storing progress as a JSON blob on the user row.

Do this instead

Use a lesson_progress table. It's easy to query, index and report on.

Your mission

First milestone: one course, real progress

~40 min

Quick check

A student clicks 'Mark as done' twice on the same lesson. With the table above, what happens?

Go deeper

  1. 1Model courses as nested data: course → modules → lessons.
  2. 2Track and persist per-user progress through course content.
  3. 3Add a quiz feature with scoring.

More projects at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge