<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: kareem khamis</title>
    <description>The latest articles on DEV Community by kareem khamis (@kareem_khamis_c236511e212).</description>
    <link>https://dev.to/kareem_khamis_c236511e212</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3866506%2F871ffe31-5e23-4e55-a5b5-a1e1273dd867.png</url>
      <title>DEV Community: kareem khamis</title>
      <link>https://dev.to/kareem_khamis_c236511e212</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kareem_khamis_c236511e212"/>
    <language>en</language>
    <item>
      <title>Qodrateman: A Full-Stack E-Learning Platform with AI Quiz Generation.</title>
      <dc:creator>kareem khamis</dc:creator>
      <pubDate>Wed, 08 Apr 2026 12:40:25 +0000</pubDate>
      <link>https://dev.to/kareem_khamis_c236511e212/qodrateman-a-full-stack-e-learning-platform-with-ai-quiz-generation-pjh</link>
      <guid>https://dev.to/kareem_khamis_c236511e212/qodrateman-a-full-stack-e-learning-platform-with-ai-quiz-generation-pjh</guid>
      <description>&lt;p&gt;I'm Karim Khamis, a full-stack developer and AI/ML researcher based in Cairo, Egypt. This is the full story of how I built &lt;strong&gt;Qodrateman&lt;/strong&gt; — a production e-learning platform now live at &lt;a href="https://qodrateman.com" rel="noopener noreferrer"&gt;qodrateman.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This isn't a tutorial project. It's a real platform serving real students across 12+ courses, with instructor and learner workflows, JWT authentication, role-based access control, and an AI feature that reduces quiz creation time from 20 minutes to under 30 seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Qodrateman?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Qodrateman&lt;/strong&gt; is a full-stack e-learning platform built for the Arabic-speaking market. It provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured courses with instructor and learner role separation&lt;/li&gt;
&lt;li&gt;JWT authentication with Role-Based Access Control (RBAC)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-assisted quiz generation&lt;/strong&gt; — instructors describe a topic and receive a complete quiz in under 30 seconds&lt;/li&gt;
&lt;li&gt;Full content management for 12+ courses&lt;/li&gt;
&lt;li&gt;Deployed on Vercel (frontend) and Render (backend) with zero downtime&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Stack and Why I Chose It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Frontend: React + TypeScript + Tailwind CSS
&lt;/h3&gt;

&lt;p&gt;React was the right call for a platform where UI state is complex — which course is active, which lesson is playing, what the user's progress is. TypeScript caught dozens of bugs before they reached production. Tailwind made responsive design fast without writing custom CSS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend: Django REST Framework
&lt;/h3&gt;

&lt;p&gt;DRF is the best choice I know for building REST APIs quickly without sacrificing quality. The combination of serializers, viewsets, and the built-in admin panel gave me a fully functional backend in a fraction of the time it would take to build from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database: PostgreSQL
&lt;/h3&gt;

&lt;p&gt;The data in an e-learning platform is deeply relational — users, courses, lessons, enrollments, quiz attempts. PostgreSQL handles this cleanly. SQLite is fine for development but never for production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication: JWT with djangorestframework-simplejwt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
&lt;/span&gt;&lt;span class="n"&gt;REST_FRAMEWORK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DEFAULT_AUTHENTICATION_CLASSES&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rest_framework_simplejwt.authentication.JWTAuthentication&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JWT tokens keep the frontend and backend fully decoupled. The access token lives in memory (not localStorage — that's a security risk). The refresh token handles silent re-authentication.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment: Vercel + Render
&lt;/h3&gt;

&lt;p&gt;Vercel for the React frontend — zero config, instant deploys on every push. Render for the Django backend — free tier works for early-stage, scales easily. Total deployment setup time: under 2 hours.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Feature I'm Most Proud Of: AI Quiz Generation
&lt;/h2&gt;

&lt;p&gt;The biggest pain point instructors told me about was quiz creation. Writing good multiple-choice questions is time-consuming — it took instructors around 20 minutes per quiz.&lt;/p&gt;

&lt;p&gt;I built an AI-assisted quiz generation feature that solves this completely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Instructor describes the topic and difficulty level&lt;/li&gt;
&lt;li&gt;The system sends a structured prompt to the AI API&lt;/li&gt;
&lt;li&gt;The API returns a complete quiz with questions, options, and correct answers in JSON format&lt;/li&gt;
&lt;li&gt;The platform parses the response and saves it directly to the database&lt;/li&gt;
&lt;li&gt;The instructor reviews and publishes in one click&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Result: quiz creation time dropped from ~20 minutes to under 30 seconds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The key to making this reliable was the structured prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_quiz_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_questions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Generate a quiz about: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
    Number of questions: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;num_questions&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
    Difficulty: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;difficulty&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;

    Respond ONLY with valid JSON in this exact format:
    {{
        &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;questions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: [
            {{
                &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,
                &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: [&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;D&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;],
                &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correct_answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,
                &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;explanation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;why this is correct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
            }}
        ]
    }}
    Do not include any text outside the JSON.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forcing JSON output and validating the response before saving was the most important engineering decision here. Early versions would occasionally return malformed responses — strict output format constraints fixed this completely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Role-Based Access Control
&lt;/h2&gt;

&lt;p&gt;Qodrateman has two distinct user types — instructors and learners — with completely different capabilities.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# models.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ROLES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;instructor&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Instructor&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;learner&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Learner&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OneToOneField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_delete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ROLES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# permissions.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IsInstructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BasePermission&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;has_permission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_authenticated&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userprofile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;instructor&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This permission class then protects every instructor-only endpoint — course creation, quiz generation, student progress visibility. Learners can only read published content and submit their own quiz attempts.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Broke and How I Fixed It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem 1: N+1 query problem in course listings
&lt;/h3&gt;

&lt;p&gt;The course listing API was slow. Every course card needed the instructor name, lesson count, and enrollment count — three separate database queries per course. With 12+ courses, that's 36+ queries per page load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Django's &lt;code&gt;select_related&lt;/code&gt; and &lt;code&gt;annotate&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;courses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_related&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;instructor&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;annotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;lesson_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lessons&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;enrollment_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;enrollments&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One query. Page load time dropped by 70%.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 2: AI responses were inconsistent
&lt;/h3&gt;

&lt;p&gt;Early AI quiz generation would sometimes return extra text around the JSON, making &lt;code&gt;json.loads()&lt;/code&gt; fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Wrap parsing in a try/except and strip everything before the first &lt;code&gt;{&lt;/code&gt; and after the last &lt;code&gt;}&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_ai_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response_text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response_text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rindex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AI response could not be parsed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Problem 3: Render backend cold starts
&lt;/h3&gt;

&lt;p&gt;Render's free tier spins down inactive services. The first request after inactivity takes 30+ seconds — terrible UX.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; A simple ping endpoint that a Vercel cron job hits every 14 minutes to keep the service warm. Cost: zero. Result: no more cold starts for active users.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Metrics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;12+ courses&lt;/strong&gt; live on the platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quiz generation time:&lt;/strong&gt; from ~20 minutes to under 30 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;500+ authenticated users&lt;/strong&gt; supported with JWT auth and RBAC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero downtime&lt;/strong&gt; since launch on Vercel + Render&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero post-launch rollbacks&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I Would Do Differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add React Query from day one.&lt;/strong&gt; Managing server state with raw &lt;code&gt;useEffect&lt;/code&gt; and &lt;code&gt;useState&lt;/code&gt; gets messy fast. React Query handles caching, refetching, and loading states much more cleanly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plan the permission model before writing any code.&lt;/strong&gt; I refactored RBAC twice because I hadn't thought through all the edge cases — can instructors see other instructors' content? Can learners see unenrolled course previews? Draw this out first.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use Docker locally from the start.&lt;/strong&gt; "Works on my machine" is a real problem. Docker Compose for local development makes deployment much smoother.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  My Other Projects
&lt;/h2&gt;

&lt;p&gt;If you're interested in AI/ML work, I'm also building &lt;strong&gt;Letra&lt;/strong&gt; — a custom-trained OCR mobile app for iOS and Android using TensorFlow, Keras, PaddleOCR, and OpenCV. Currently at 98% English accuracy on 800+ labeled samples, deployed to TestFlight and Google Play Beta. Arabic OCR is in active iteration targeting 85%+ accuracy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Qodrateman taught me that the hardest part of building a real product isn't the code — it's the decisions you make before you write any code. Schema design, permission models, deployment architecture. Get these right first and the code almost writes itself.&lt;/p&gt;

&lt;p&gt;If you're building something similar or have questions about the Django + React stack, drop a comment. I'm happy to help.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;About the author:&lt;/strong&gt;&lt;br&gt;
Karim Khamis is a full-stack developer and AI/ML researcher based in Cairo, Egypt. He holds a B.Sc. in Software Engineering from Ain Shams University and is pursuing an M.Sc. at AASTMT researching OCR and multilingual document processing. He has delivered 30+ client projects with a 100% on-time delivery rate and is available for freelance and full-time roles worldwide.&lt;/p&gt;

&lt;p&gt;🌐 Portfolio: &lt;a href="https://www.karimkhamis.com" rel="noopener noreferrer"&gt;karimkhamis.com&lt;/a&gt;&lt;br&gt;
🚀 Qodrateman: &lt;a href="https://qodrateman.com" rel="noopener noreferrer"&gt;qodrateman.com&lt;/a&gt;&lt;br&gt;
🐙 GitHub: &lt;a href="https://github.com/karim-99-99" rel="noopener noreferrer"&gt;github.com/karim-99-99&lt;/a&gt;&lt;br&gt;
💼 LinkedIn: &lt;a href="https://linkedin.com/in/kareem-khamis" rel="noopener noreferrer"&gt;linkedin.com/in/kareem-khamis&lt;/a&gt;&lt;br&gt;
📧 Email: &lt;a href="mailto:kareemkhamis2030@gmail.com"&gt;kareemkhamis2030@gmail.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>django</category>
      <category>python</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
