Platziflix es una plataforma de cursos online estilo Netflix con arquitectura multi-plataforma:
- Backend: API REST con FastAPI + PostgreSQL (puerto 8000)
- Frontend: Aplicación web con Next.js 15 (puerto 3000)
- Mobile Android: App nativa Kotlin + Jetpack Compose
- Mobile iOS: App nativa Swift + SwiftUI
El Backend es la única fuente de verdad de datos. Toda la UI (web y mobile) consume exclusivamente la API REST.
claude-code/
├── CLAUDE.md
├── Backend/
│ ├── app/
│ │ ├── main.py # FastAPI app + todos los endpoints
│ │ ├── test_main.py # Tests de endpoints principales
│ │ ├── alembic.ini
│ │ ├── alembic/versions/ # Migraciones de DB
│ │ ├── core/config.py # Settings con Pydantic
│ │ ├── db/
│ │ │ ├── base.py # Engine + SessionLocal + get_db()
│ │ │ └── seed.py # Datos de prueba
│ │ ├── models/ # SQLAlchemy models
│ │ ├── schemas/rating.py # Pydantic schemas para ratings
│ │ ├── services/course_service.py # Lógica de negocio
│ │ └── tests/ # Tests de integración y servicio
│ ├── specs/ # Contratos y especificaciones API
│ ├── docker-compose.yml
│ ├── Dockerfile
│ ├── Makefile
│ └── pyproject.toml
├── Frontend/
│ └── src/
│ ├── app/ # Next.js App Router
│ │ ├── page.tsx # Home (listado de cursos)
│ │ ├── course/[slug]/page.tsx # Detalle de curso
│ │ └── classes/[class_id]/ # Reproductor de clase
│ ├── components/
│ │ ├── Course/ # Card de curso
│ │ ├── CourseDetail/ # Header/detalle de curso
│ │ ├── StarRating/ # Estrellas de rating
│ │ └── VideoPlayer/ # Reproductor HTML5
│ ├── services/ratingsApi.ts # Integración API ratings
│ ├── styles/ # vars.scss + reset.scss globales
│ └── types/ # Interfaces TypeScript
└── Mobile/
├── PlatziFlixAndroid/ # Clean Architecture + Jetpack Compose
└── PlatziFlixiOS/ # Domain-driven + SwiftUI
| Capa | Tecnología | Versión |
|---|---|---|
| Backend Framework | FastAPI | ≥0.104 |
| Backend Runtime | Python | ≥3.11 |
| ORM | SQLAlchemy | 2.0 |
| Migraciones | Alembic | ≥1.13 |
| Base de Datos | PostgreSQL | 15 |
| Container | Docker + Docker Compose | - |
| Dep. Management (BE) | UV | - |
| Frontend Framework | Next.js | 15.3.3 |
| UI Library | React | 19.0 |
| Lenguaje (FE) | TypeScript strict | 5.x |
| Estilos | SCSS + CSS Modules | - |
| Testing (FE) | Vitest + React Testing Library | 3.x / 16.x |
| Android UI | Jetpack Compose + Material3 | - |
| Android Network | Retrofit + OkHttp | 2.9 / 4.12 |
| Android Images | Coil | 2.5 |
| iOS UI | SwiftUI + Combine | - |
| iOS Network | URLSession + Codable (nativo) | - |
Teacher
├── id (PK)
├── name (str, NOT NULL)
├── email (str, UNIQUE, indexed)
└── [created_at, updated_at, deleted_at] ← heredado de BaseModel
Course
├── id (PK)
├── name (str)
├── description (text)
├── thumbnail (str, URL)
├── slug (str, UNIQUE, indexed) ← usado para URLs SEO-friendly
├── teachers → M:M via course_teachers
├── lessons → 1:M (cascade delete)
├── ratings → 1:M (cascade delete)
├── average_rating [computed property]
└── total_ratings [computed property]
Lesson
├── id (PK)
├── course_id (FK → courses, indexed)
├── name (str)
├── description (text)
├── slug (str, indexed)
├── video_url (str)
└── [timestamps]
CourseRating
├── id (PK)
├── course_id (FK → courses, indexed)
├── user_id (int, indexed) ← sin FK, sistema de usuarios externo
├── rating (int, CHECK: 1-5)
├── UNIQUE(course_id, user_id, deleted_at) ← soporta soft delete
└── [timestamps]
course_teachers [tabla asociativa]
├── course_id (FK, PK)
└── teacher_id (FK, PK)
Todas las entidades heredan deleted_at (nullable). Los registros borrados no se eliminan físicamente.
| Método | Ruta | Descripción | Respuesta |
|---|---|---|---|
| GET | / |
Bienvenida | 200 |
| GET | /health |
Health check + DB | 200 |
| GET | /courses |
Lista cursos activos | 200 [Course] |
| GET | /courses/{slug} |
Detalle curso + teachers + lessons | 200/404 |
| GET | /classes/{class_id} |
Detalle lección por ID | 200/404 |
| POST | /courses/{course_id}/ratings |
Crear/actualizar rating | 201/400/404 |
| GET | /courses/{course_id}/ratings |
Ratings activos del curso | 200/404 |
| GET | /courses/{course_id}/ratings/stats |
Estadísticas agregadas | 200/404 |
| GET | /courses/{course_id}/ratings/user/{user_id} |
Rating de usuario específico | 200/204 |
| PUT | /courses/{course_id}/ratings/{user_id} |
Actualizar rating | 200/400/404 |
| DELETE | /courses/{course_id}/ratings/{user_id} |
Soft delete rating | 204/404 |
GET /courses → array de:
{ "id", "name", "description", "thumbnail", "slug", "average_rating", "total_ratings" }GET /courses/{slug} → objeto con:
{ ...course, "teacher_id": [...], "classes": [...], "rating_distribution": {1:n, 2:n, ...}, "average_rating", "total_ratings" }Rating Request Body:
{ "user_id": int (>0), "rating": int (1-5) }Rating Stats Response:
{ "average_rating": float, "total_ratings": int, "rating_distribution": {1: int, 2: int, 3: int, 4: int, 5: int} }Request → FastAPI Router (main.py)
→ Dependency Injection (get_db, get_course_service)
→ CourseService (services/course_service.py) ← lógica de negocio
→ SQLAlchemy ORM (models/)
→ PostgreSQL
- Service Layer:
CourseServiceconcentra toda la lógica de negocio - Dependency Injection:
FastAPI Depends()para DB session y servicios - Soft Delete:
deleted_aten lugar deDELETEfísico - Eager Loading:
joinedload()enget_course_by_slugpara evitar N+1 - Agregación en DB: Rating stats calculados con SQL GROUP BY
d18a08253457— Schema inicial: teachers, courses, lessons, course_teachers0e3a8766f785— Tabla course_ratings con CHECK constraint
Server Component (page.tsx)
→ fetch('http://localhost:8000/...')
→ Render SSR
→ Hydration en cliente
Las páginas de listado y detalle son Server Components. El ratingsApi.ts es para llamadas cliente.
| Componente | Ubicación | Props Clave |
|---|---|---|
Course |
components/Course/ |
id, name, description, thumbnail, average_rating, total_ratings |
CourseDetail |
components/CourseDetail/ |
course: CourseDetail (incluye classes[]) |
StarRating |
components/StarRating/ |
rating, totalRatings?, showCount?, size?, readonly? |
VideoPlayer |
components/VideoPlayer/ |
src, title |
// src/types/index.ts
interface Course { id, name, description, thumbnail, slug, average_rating?, total_ratings? }
interface Class { id, title, description, video, duration (seconds), slug }
interface CourseDetail extends Course { classes: Class[] }
// src/types/rating.ts
interface CourseRating { id, course_id, user_id, rating (1-5), created_at, updated_at }
interface RatingStats { average_rating, total_ratings }
type RatingState = 'idle' | 'loading' | 'success' | 'error'
class ApiError extends Error { status, code?, details? }- Variables SCSS en
src/styles/vars.scss— auto-importadas en todos los.module.scssvianext.config.ts - Color primario Platzi:
#ff2d2d - Breakpoint responsive:
max-width: 768px
Arquitectura: Clean Architecture en 3 capas
Presentation (Compose + ViewModel + MVI)
↓ Domain Models
Domain (Interfaces Repository + Models puros)
↑ DTOs mapeados
Data (Retrofit + DTOs + Mappers + RemoteRepository)
Archivos clave:
MainActivity.kt— Entry point, inyecta ViewModel manualmente viaAppModuleCourseListViewModel.kt— MVI conStateFlow<CourseListUiState>+handleEvent()ApiService.kt— Interface Retrofit:GET /coursesNetworkModule.kt— Singleton, base URLhttp://10.0.2.2:8000/(emulador usa 10.0.2.2, no localhost)RemoteCourseRepository.kt— Implementación real, retornaResult<List<Course>>MockCourseRepository.kt— Para testing, simula delay y fallo aleatorio 10%CourseMapper.kt—CourseDTO → Course
Estado UI:
data class CourseListUiState(isLoading, courses, error, isRefreshing)
sealed class CourseListUiEvent { LoadCourses, RefreshCourses, ClearError }Pantallas Compose:
CourseListScreen— Scaffold + LargeTopAppBar + LazyColumnCourseCard— Card con thumbnail 16:9 via Coil + AsyncImage
Nota: Navegación al detalle de curso está marcada como TODO.
Arquitectura: Domain-driven con protocolos Swift
Presentation (SwiftUI Views + ViewModel @MainActor)
↓ Domain Models (Struct Identifiable)
Domain (Protocol CourseRepositoryProtocol + Models)
↑ DTOs + Mappers
Data (NetworkManager → URLSession + Codable)
Archivos clave:
CourseListViewModel.swift—@MainActor ObservableObject, async/await + Combine para debounce búsquedaRemoteCourseRepository.swift— ImplementaCourseRepositoryProtocolCourseAPIEndpoints.swift— Enum congetAllCourses/getCourseBySlugbase URLhttp://localhost:8000NetworkManager.swift— SingletonURLSessionwrapperNetworkError.swift— Errores tipados con mensajes en español
Vistas SwiftUI:
CourseListView—NavigationView+LazyVStack+ búsqueda + pull-to-refreshCourseCardView—AsyncImage16:9 + nombre + descripciónDesignSystem.swift— Constantes de spacing, colores, tipografía
No usa dependencias externas — solo frameworks nativos iOS (Foundation, SwiftUI, Combine).
cd Backend
make start # docker-compose up -d
make stop # docker-compose down
make restart # docker-compose restart
make build # docker-compose build
make logs # docker-compose logs -f
make clean # docker-compose down -v --rmi all
make migrate # alembic upgrade head (dentro del contenedor)
make create-migration # crear nueva migración (prompt interactivo)
make seed # poblar datos de prueba
make seed-fresh # limpiar y repoblar
make help # ver todos los comandosIMPORTANTE: Antes de ejecutar cualquier comando de Backend, verificar que el contenedor Docker esté corriendo (make start). Revisar el Makefile para los comandos disponibles.
cd Frontend
yarn dev # Servidor desarrollo (turbopack)
yarn build # Build producción
yarn start # Iniciar producción
yarn test # Vitest
yarn lint # ESLint| Servicio | URL |
|---|---|
| Backend API | http://localhost:8000 |
| API Docs (Swagger) | http://localhost:8000/docs |
| Frontend Web | http://localhost:3000 |
| Android emulador → Backend | http://10.0.2.2:8000 |
| iOS simulator → Backend | http://localhost:8000 |
app/test_main.py— Tests endpoints raíz, health, cursos (mock del servicio)app/tests/test_rating_endpoints.py— Integración endpoints ratingapp/tests/test_course_rating_service.py— Unit tests servicioapp/tests/test_rating_db_constraints.py— Constraints de DB
Course.test.tsx— Renderizado de card (3 casos)StarRating.test.tsx— Rendering, accesibilidad, variantes de tamaño (~45 casos)VideoPlayer.test.tsx— Elemento video básicoclasses/[class_id]/page.test.tsx— SSR de página con mock de fetch
CourseListViewModelTest.kt— JUnit + coroutines test
PlatziFlixiOSTests.swift— XCTest estándar
- Docker obligatorio para el backend — DB y API corren en containers
- API REST es la única fuente de datos para Frontend y Mobile
- TypeScript strict en Frontend — no usar
any - Testing requerido para nuevas funcionalidades
- Migraciones automáticas para cualquier cambio de schema de DB — nunca modificar la DB directamente
- Soft delete en todas las entidades — nunca hacer DELETE físico
- Snake_case en Python/SQL, camelCase en JS/TS, PascalCase en Swift/Kotlin
- SCSS vars auto-importadas — no importar
vars.scssmanualmente en módulos - Android emulador usa
10.0.2.2para referirse al localhost del host - Idioma UI: Español (
lang="es", mensajes de error en español)
- Catálogo de cursos con grid estilo Netflix
- Detalle de cursos (profesores, lecciones, clases)
- Navegación por slug SEO-friendly
- Reproductor de video integrado (HTML5)
- Sistema de ratings 1-5 estrellas con estadísticas
- Distribución de ratings por estrella
- Health checks de API y DB
- Apps móviles nativas (Android lista, iOS con búsqueda y pull-to-refresh)
- Soft delete en entidades de ratings
- Testing en todos los niveles del stack