Remember when I thought building a Learning Management System would be "just another CRUD app"? Yeah, I was wrong. Dead wrong.
After spending the last three months architecting and building a full-featured LMS from scratch, I've learned more about mobile architecture, state management, and user experience than I did in my entire bootcamp. And honestly? I'm kind of obsessed with how it turned out.
Let me walk you through exactly how I built this thing, from folder structure to the pixel-perfect UI that made my designer friends jealous.
Why Another LMS? (And Why React Native?)
Look, I know what you're thinking. "There are already a million LMS platforms out there." You're right. But here's the thing—most of them feel like they were designed in 2015 and never updated. Clunky interfaces, terrible mobile experiences, and honestly, they just aren't fun to use.
I wanted to build something that felt native, looked gorgeous, and actually made learning enjoyable. React Native with Expo was the obvious choice because:
- True native performance without the overhead of maintaining separate codebases
- Hot reload that actually works (looking at you, Flutter)
- Massive ecosystem and community
- Easy deployment with EAS Build
Plus, I'm a JavaScript developer at heart, so why fight it?
The Architecture That Actually Scales
Before writing a single line of code, I spent two weeks just architecting this thing. Here's the high-level system design that's been bulletproof so far:
System Architecture Overview
┌─────────────────┐
│ Mobile App │
│ (React Native) │
└────────┬────────┘
│
┌────┴────┐
│ API │
│ Gateway │
└────┬────┘
│
┌────┴──────────────────────┐
│ │
┌───┴────┐ ┌──────┴─────┐
│ Auth │ │ Content │
│ Service│ │ Service │
└───┬────┘ └──────┬─────┘
│ │
┌───┴────────────┐ ┌────────┴─────┐
│ PostgreSQL │ │ Firebase │
│ (User Data) │ │ (Media/CDN) │
└────────────────┘ └──────────────┘
I went with a microservices-inspired approach, but kept it pragmatic. No need to over-engineer for the sake of buzzwords.
Key Architecture Decisions:
- Backend: Node.js with Express (I considered NestJS but decided against the overhead)
- Database: PostgreSQL for relational data, Redis for caching
- File Storage: Firebase Storage (S3 would work too, but Firebase integration is chef's kiss)
- Real-time: Socket.io for live classes and chat
- Auth: JWT with refresh tokens, stored in secure storage
The Folder Structure That Saved My Sanity
I've seen some absolutely chaotic React Native projects. Files everywhere, no clear separation of concerns, and good luck finding that one component you need.
Here's the structure I landed on after multiple iterations:
eduflow/
├── src/
│ ├── app/ # Expo Router pages
│ │ ├── (auth)/
│ │ │ ├── login.tsx
│ │ │ ├── register.tsx
│ │ │ └── forgot-password.tsx
│ │ ├── (tabs)/ # Main tab navigation
│ │ │ ├── index.tsx # Home/Dashboard
│ │ │ ├── courses.tsx
│ │ │ ├── my-learning.tsx
│ │ │ └── profile.tsx
│ │ ├── course/
│ │ │ ├── [id].tsx # Course details
│ │ │ └── lesson/[id].tsx
│ │ └── _layout.tsx
│ │
│ ├── components/
│ │ ├── ui/ # Reusable UI components
│ │ │ ├── Button.tsx
│ │ │ ├── Card.tsx
│ │ │ ├── Input.tsx
│ │ │ ├── Typography.tsx
│ │ │ └── Avatar.tsx
│ │ ├── course/ # Course-specific components
│ │ │ ├── CourseCard.tsx
│ │ │ ├── LessonList.tsx
│ │ │ ├── VideoPlayer.tsx
│ │ │ └── ProgressBar.tsx
│ │ ├── learning/
│ │ │ ├── QuizCard.tsx
│ │ │ ├── AssignmentCard.tsx
│ │ │ └── CertificateView.tsx
│ │ └── common/
│ │ ├── Header.tsx
│ │ ├── LoadingScreen.tsx
│ │ └── EmptyState.tsx
│ │
│ ├── features/ # Feature-based modules
│ │ ├── auth/
│ │ │ ├── hooks/
│ │ │ ├── services/
│ │ │ └── types.ts
│ │ ├── courses/
│ │ │ ├── hooks/
│ │ │ ├── services/
│ │ │ └── types.ts
│ │ └── learning/
│ │ ├── hooks/
│ │ ├── services/
│ │ └── types.ts
│ │
│ ├── store/ # State management (Zustand)
│ │ ├── authStore.ts
│ │ ├── courseStore.ts
│ │ ├── learningStore.ts
│ │ └── uiStore.ts
│ │
│ ├── services/ # API and external services
│ │ ├── api/
│ │ │ ├── client.ts
│ │ │ ├── auth.ts
│ │ │ ├── courses.ts
│ │ │ └── user.ts
│ │ ├── storage/
│ │ │ └── secureStorage.ts
│ │ └── analytics/
│ │ └── analytics.ts
│ │
│ ├── utils/ # Utility functions
│ │ ├── formatters.ts
│ │ ├── validators.ts
│ │ └── helpers.ts
│ │
│ ├── hooks/ # Shared custom hooks
│ │ ├── useAuth.ts
│ │ ├── useTheme.ts
│ │ └── useNetworkStatus.ts
│ │
│ ├── constants/
│ │ ├── colors.ts
│ │ ├── typography.ts
│ │ └── layout.ts
│ │
│ └── types/ # Global TypeScript types
│ ├── models.ts
│ └── api.ts
│
├── assets/
│ ├── fonts/
│ ├── images/
│ └── animations/ # Lottie files
│
├── app.json
├── package.json
└── tsconfig.json
This structure follows a few key principles:
- Feature-first organization: Related code stays together
- Clear separation of concerns: UI, business logic, and data are distinct
- Scalability: Easy to add new features without refactoring
- TypeScript everywhere: Type safety is non-negotiable
The Tech Stack (And Why Each Choice Matters)
I'm not going to bore you with the usual "we chose X because it's popular" nonsense. Every technology here earned its place through trial and error.
Core Technologies
React Native with Expo SDK 51
- File-based routing with Expo Router (game changer)
- EAS Build for CI/CD
- Expo modules for native functionality
State Management: Zustand
- Redux was overkill
- Context API doesn't scale well
- Zustand hits the sweet spot—simple, performant, minimal boilerplate
Styling: NativeWind (Tailwind CSS)
- Utility-first approach
- Consistent design system
- Works beautifully with dark mode
Navigation: Expo Router
- File-based routing (like Next.js but for mobile)
- Type-safe navigation
- Deep linking out of the box
Animations: Reanimated 3
- 60fps animations
- Runs on UI thread
- Makes the app feel premium
Forms: React Hook Form + Zod
- Performant form handling
- Type-safe validation
- Great DX
Data Fetching: TanStack Query (React Query)
- Automatic caching
- Background refetching
- Optimistic updates
Backend & Infrastructure
- API: Node.js + Express + TypeScript
- Database: PostgreSQL with Prisma ORM
- Caching: Redis
- File Storage: Firebase Storage
- CDN: Cloudflare
- Real-time: Socket.io
- Search: Algolia (because implementing good search is harder than you think)
The UI/UX Philosophy: Less Is Actually More
I spent an embarrassing amount of time on design. Like, way more than I should have. But it was worth it.
Design Principles
1. Minimalist but Warm
- Clean interfaces with plenty of breathing room
- Subtle gradients instead of flat colors
- Rounded corners everywhere (16px radius is my religion now)
2. Typography-First
- Inter for UI elements
- Merriweather for long-form content
- Clear hierarchy with size and weight
3. Gesture-Driven
- Swipe to bookmark courses
- Pull to refresh
- Long-press for quick actions
- Every interaction should feel natural
4. Dark Mode Done Right
- Not just inverted colors
- Thoughtful contrast ratios
- True black (#000) for OLED screens
5. Micro-interactions
- Haptic feedback on important actions
- Loading states that don't feel like loading
- Success animations that make you smile
Color Palette
// Light Mode
primary: '#2563EB', // Electric blue
secondary: '#7C3AED', // Deep purple
accent: '#F59E0B', // Warm amber
success: '#10B981', // Fresh green
error: '#EF4444', // Vibrant red
background: '#FFFFFF',
surface: '#F9FAFB',
text: '#111827',
// Dark Mode
primary: '#3B82F6',
secondary: '#8B5CF6',
accent: '#FBBF24',
success: '#34D399',
error: '#F87171',
background: '#000000',
surface: '#1F2937',
text: '#F9FAFB',
Key Features That Make It Shine
1. Smart Course Discovery
Instead of just listing courses, I built a recommendation engine that actually works:
- Analyzes user behavior (courses viewed, completed, bookmarked)
- Factors in skill level and learning pace
- Suggests courses based on completion patterns
- Updates in real-time as you browse
2. Offline-First Architecture
Because not everyone has reliable internet:
- Download courses for offline viewing
- Queue actions when offline (bookmarks, progress updates)
- Sync automatically when connection returns
- Smart storage management (clears old downloads)
3. Interactive Video Player
This was a nightmare to build but users love it:
- Variable playback speed
- Picture-in-picture mode
- Thumbnail scrubbing
- Automatic quality adjustment
- Chapter markers
- Note-taking without pausing
4. Gamification That Doesn't Suck
Most apps go overboard with badges and points. I kept it subtle:
- Streak tracking (simple but effective)
- Progress visualization
- Milestone celebrations
- Shareable certificates
- No annoying popups
5. Live Classes
Real-time video streaming with:
- Screen sharing
- Interactive whiteboard
- Chat with emoji reactions
- Polls and quizzes
- Recording for later viewing
The Code That Powers It All
Let me show you some actual implementation details. Here's how the course store is structured with Zustand:
// store/courseStore.ts
interface CourseStore {
courses: Course[];
loading: boolean;
error: string | null;
fetchCourses: () => Promise<void>;
enrollCourse: (courseId: string) => Promise<void>;
updateProgress: (courseId: string, lessonId: string, progress: number) => void;
}
export const useCourseStore = create<CourseStore>((set, get) => ({
courses: [],
loading: false,
error: null,
fetchCourses: async () => {
set({ loading: true });
try {
const courses = await courseApi.getAll();
set({ courses, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
enrollCourse: async (courseId) => {
await courseApi.enroll(courseId);
// Optimistic update
set((state) => ({
courses: state.courses.map((course) =>
course.id === courseId
? { ...course, enrolled: true }
: course
),
}));
},
updateProgress: (courseId, lessonId, progress) => {
set((state) => ({
courses: state.courses.map((course) =>
course.id === courseId
? {
...course,
lessons: course.lessons.map((lesson) =>
lesson.id === lessonId
? { ...lesson, progress }
: lesson
),
}
: course
),
}));
},
}));
Clean, type-safe, and easy to test.
Performance Optimizations That Actually Matter
I obsessed over performance. Here's what moved the needle:
1. Image Optimization
- WebP format with JPEG fallback
- Lazy loading with blurhash placeholders
- Responsive images based on device resolution
- Aggressive caching strategy
2. List Virtualization
- FlashList instead of FlatList (3x better performance)
- Proper key extraction
- Memoized render items
3. Code Splitting
- Lazy load screens
- Dynamic imports for heavy components
- Route-based splitting with Expo Router
4. Animation Performance
- Use Reanimated for all animations
- Avoid animating height/width (use scale)
- Run animations on UI thread
- Reduce overdraw with
removeClippedSubviews
5. Network Optimization
- Request deduplication
- Optimistic updates
- Background fetch for fresh content
- Smart retry with exponential backoff
Deployment & DevOps
Setting up the deployment pipeline was surprisingly smooth with Expo:
# EAS Build configuration
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {
"android": {
"serviceAccountKeyPath": "./secrets/play-store.json"
},
"ios": {
"appleId": "your@email.com",
"ascAppId": "123456789",
"appleTeamId": "ABCD123"
}
}
}
}
Continuous deployment pipeline:
- Push to main branch
- GitHub Actions triggers
- Run tests and linting
- Build with EAS
- Deploy to staging
- Automatic submission to stores (with approval)
Lessons Learned (The Hard Way)
What Worked
- Starting with TypeScript from day one
- Investing time in architecture upfront
- Building a design system before features
- Writing tests for critical paths
- User testing early and often
What Didn't Work
- Trying to build everything myself (use libraries!)
- Over-engineering the state management
- Neglecting Android testing until late
- Assuming users have good internet
- Skipping accessibility initially
Biggest Surprises
- Animations make a HUGE difference in perceived performance
- Users barely notice features but always notice bugs
- Dark mode is non-negotiable in 2025
- Offline support is harder than expected but worth it
- Good error messages reduce support requests by 60%
What's Next?
The app is live on both stores, and I'm blown away by the response. But I'm not done. Here's what's coming:
- AI-powered study assistant
- Social learning features (study groups, peer reviews)
- AR/VR support for immersive learning
- Blockchain certificates (yes, actually useful here)
- Instructor dashboard and course creation tools
Final Thoughts
Building this LMS taught me that the difference between a good app and a great app isn't just features—it's the tiny details. The smooth animations, the thoughtful error messages, the intuitive gestures.
If you're thinking about building something similar, my advice: start with the user experience, obsess over the details, and don't ship until it feels magical.
Is this the perfect architecture? Probably not. Will I refactor parts of it in six months? Definitely. But it works, it scales, and most importantly, people love using it.
That's good enough for me.
That's a wrap 🎁
Now go touch some code 👨💻
Top comments (0)