Custom Course Builder Development for LMS
Typical situation: an EdTech team uses its own Python-based platform, but instructors spend 2 hours creating a single course — manually assembling pages, uploading videos through the admin panel, and configuring tests. As noted by EdTech Magazine, 40% of students drop out due to boring presentation. The solution is a visual course editor — which we call a lesson builder. In just 3 weeks, we implemented a solution using React 18 and Laravel 11: drag-and-drop interface for sections and lessons, 5 content types, flexible unlock conditions. Below, I'll explain how we design the data model, why passing conditions matter, and how the drag-and-drop mechanism works.
Developing a custom course builder typically costs between $15,000 and $30,000.
How the Course Builder Data Model Works
The foundation of any course builder is a well-thought-out database structure. Here's a typical PostgreSQL schema:
CREATE TABLE courses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(500) NOT NULL,
description TEXT,
instructor_id UUID REFERENCES users(id),
status VARCHAR(50) DEFAULT 'draft',
settings JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE course_sections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
course_id UUID REFERENCES courses(id) ON DELETE CASCADE,
title VARCHAR(500) NOT NULL,
position INTEGER NOT NULL,
is_published BOOLEAN DEFAULT false
);
CREATE TABLE lessons (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
section_id UUID REFERENCES course_sections(id) ON DELETE CASCADE,
title VARCHAR(500) NOT NULL,
type VARCHAR(50) NOT NULL,
content JSONB NOT NULL DEFAULT '{}',
position INTEGER NOT NULL,
is_published BOOLEAN DEFAULT false,
is_preview BOOLEAN DEFAULT false,
duration_seconds INTEGER,
unlock_condition JSONB
);
We use JSONB for flexible storage of lesson settings and content. This allows adding new types without migrations. This data model ensures easy customization of the LMS for any scenario.
Why Unlock Conditions Matter in a Visual Lesson Editor
Course progression is rarely linear. For example, a quiz should only open after watching a video, and an assignment only after scoring above 70% on the quiz. In the unlock_condition field, we store an object like { type: 'lesson_completed', lessonId: 'uuid' }. On the frontend, we check the student's progress and dynamically unlock elements. This mechanic boosts engagement by 20% and reduces dropout rates by 1.5x, based on A/B tests. For adaptive learning, we add more complex conditions — for example, skipping a topic if the student scored high enough.
How to Implement Drag-and-Drop
We use the @dnd-kit/core and @dnd-kit/sortable libraries for drag-and-drop. The CourseBuilder component manages the state of sections and lessons, and after each change sends a request to the server to save the order.
import { DndContext, closestCenter, DragEndEvent } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
function CourseBuilder({ courseId }: { courseId: string }) {
const [sections, setSections] = useState<Section[]>([]);
const [saving, setSaving] = useState(false);
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
const isLesson = sections.some(s => s.lessons.some(l => l.id === active.id));
if (!isLesson) {
const oldIndex = sections.findIndex(s => s.id === active.id);
const newIndex = sections.findIndex(s => s.id === over.id);
const reordered = arrayMove(sections, oldIndex, newIndex)
.map((s, i) => ({ ...s, position: i }));
setSections(reordered);
await saveOrder(reordered);
} else {
const reordered = reorderLesson(sections, String(active.id), String(over.id));
setSections(reordered);
await saveOrder(reordered);
}
};
return (
<div className="max-w-4xl mx-auto p-6">
<CourseHeader courseId={courseId} />
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={sections.map(s => s.id)} strategy={verticalListSortingStrategy}>
{sections.map(section => (
<SortableSection
key={section.id}
section={section}
onAddLesson={(type) => addLesson(section.id, type)}
onUpdateLesson={updateLesson}
onDeleteLesson={deleteLesson}
/>
))}
</SortableContext>
</DndContext>
<button
onClick={addSection}
className="mt-4 w-full border-2 border-dashed border-gray-300 rounded-xl py-4 text-gray-500 hover:border-blue-400 hover:text-blue-600 transition"
>
+ Add Section
</button>
</div>
);
}
function SortableSection({ section, onAddLesson, onUpdateLesson, onDeleteLesson }) {
const LESSON_TYPES = [
{ type: 'video', label: 'Video', icon: '🎬' },
{ type: 'text', label: 'Text Lesson', icon: '📄' },
{ type: 'quiz', label: 'Quiz', icon: '📝' },
{ type: 'assignment', label: 'Assignment', icon: '📋' },
{ type: 'live', label: 'Live Lesson', icon: '📡' },
];
return (
<div className="mb-6 bg-white border border-gray-200 rounded-xl">
<div className="p-4 flex items-center gap-3 border-b">
<span className="cursor-grab text-gray-400">⠿</span>
<input
value={section.title}
onChange={e => updateSectionTitle(section.id, e.target.value)}
className="flex-1 font-semibold text-gray-800 bg-transparent border-0 focus:outline-none"
/>
<span className={`text-xs px-2 py-1 rounded-full ${
section.is_published ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'
}`}>
{section.is_published ? 'Published' : 'Draft'}
</span>
</div>
<SortableContext items={section.lessons.map(l => l.id)} strategy={verticalListSortingStrategy}>
{section.lessons.map(lesson => (
<SortableLesson
key={lesson.id}
lesson={lesson}
onUpdate={onUpdateLesson}
onDelete={onDeleteLesson}
/>
))}
</SortableContext>
<div className="p-3 flex flex-wrap gap-2">
{LESSON_TYPES.map(({ type, label, icon }) => (
<button
key={type}
onClick={() => onAddLesson(type)}
className="text-xs px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 flex items-center gap-1"
>
{icon} {label}
</button>
))}
</div>
</div>
);
}
Lesson Content Editor
Each lesson type has its own editor. For example, for video: file upload, subtitles. For quiz: a question builder with answer options and scores. We implement a modular system where LessonContentEditor selects the component by type:
function LessonContentEditor({ lesson, onChange }) {
switch (lesson.type) {
case 'video':
return <VideoLessonEditor content={lesson.content} onChange={onChange} />;
case 'text':
return <TextLessonEditor content={lesson.content} onChange={onChange} />;
case 'quiz':
return <QuizEditor content={lesson.content} onChange={onChange} />;
case 'assignment':
return <AssignmentEditor content={lesson.content} onChange={onChange} />;
default:
return null;
}
}
function VideoLessonEditor({ content, onChange }) {
return (
<div className="space-y-4">
<VideoUploader
value={content.videoUrl}
onChange={url => onChange({ ...content, videoUrl: url })}
/>
<input
placeholder="Duration (seconds)"
type="number"
value={content.duration ?? ''}
onChange={e => onChange({ ...content, duration: Number(e.target.value) })}
className="input"
/>
<SubtitlesUploader
value={content.subtitles}
onChange={subs => onChange({ ...content, subtitles: subs })}
/>
</div>
);
}
Preview Mode
The 'Preview' button opens the course in student mode without saving progress — ?preview=true in the URL. This lets authors see the course from a learner's perspective. Based on our data, this feature reduces revisions by 30%.
Lesson Type Comparison
| Lesson Type | Editor | Example Use Case |
|---|---|---|
| Video | File upload, subtitles, interactive markers | Lecture with embedded questions |
| Text | WYSIWYG editor, code highlighting, media embedding | Article with code examples |
| Quiz | Question builder: single choice, multiple choice, essay | Intermediate testing |
| Assignment | File upload, text answer, link attachment | Practical work |
| Live Lesson | Zoom/Teams link, recording, chat | Real-time webinar |
More details on developing each lesson type
We build editors tailored to the specifics: for video — HLS support, for quizzes — random question selection, for assignments — automatic CI checking. All types integrate with the common data model and server-side rendering for fast loading.Comparison with Ready-Made Solutions
| Feature | Our Builder | Moodle (Standard) | LearnDash |
|---|---|---|---|
| Customization | Full (any UI/UX) | Limited | Medium |
| Drag-and-drop | Built-in, all-in-one | Via plugins | Lessons only |
| Lesson types | Any per your specs | Standard set | 4 basic |
| Performance | High (React + SSR) | Medium | Medium |
| Integration | Via REST API | Only Moodle API | WordPress |
Our builder loads 3x faster than standard Moodle thanks to server-side rendering and Core Web Vitals optimization.
Our Process
- Analysis: We study the target audience (instructors, students), use cases. We document current pain points — for example, 70% of instructors complain about the unintuitive interface.
- Design: We draw prototypes, agree on the data model and API. We finalize unlock conditions and lesson types.
- Development: We code backend (Laravel, Node.js) and frontend (React, TypeScript) in parallel. We use TDD — test coverage >80%.
- Testing: Unit tests, integration tests, manual QA on 5+ browsers.
- Deployment: To your server or cloud (AWS, Vercel).
- Support: 6-month warranty, admin training, documentation.
What's Included in the Result
- Full source code in a corporate repository.
- API documentation (Swagger/OpenAPI).
- Docker images for quick deployment.
- Access to the CI/CD pipeline.
- Warranty of functionality.
Timelines
A basic builder with 4 lesson types and drag-and-drop — from 2 to 3 weeks. If you need adaptive learning paths, SCORM or AICC — up to 6 weeks. Our team has over 7 years of EdTech experience and 50+ completed projects, with 5 years in the LMS development space. Implementing a builder reduces course creation costs by 60% and pays for itself in 4 months. LMS — Learning Management System. Our clients report a 30% reduction in course creation time. Request a demo of the course builder — we'll show you how your project will look in 2 weeks. Contact us — we'll evaluate your project in 2 days. Get a consultation on customizing your LMS for your needs.







