AAI | IAA

Desarrollo: Aplicación profesional de soporte técnico. Un resumen con IA.
Desarrollo: Aplicación profesional de soporte técnico. Un resumen con IA.
APP · FASTAPI · NEXTJS · POSTGRESQL

Desarrollo: Aplicación profesional de soporte técnico. Un resumen con IA.


DOCUMENTO 1: ANÁLISIS EXHAUSTIVO POR VIDEO

DOCUMENTO 1: ANÁLISIS EXHAUSTIVO POR VIDEO

Serie FastAPI y Next.js - Crea una App Full Stack Real

Canal: Código para Principiantes


📹 VIDEO 1: FastAPI y Next.js - Crea una App Full Stack Real (con WebSockets)

📊 INFORMACIÓN GENERAL

  • URL: https://www.youtube.com/watch?v=5oNktUuWBMY
  • Duración: 10:13 minutos
  • Vistas: 650 vistas
  • Publicado: Hace 3 meses
  • Me gusta: 55
  • Canal: Código para Principiantes (3.78K suscriptores)
  • Hashtags: #FastAPI #NextJS #PostgreSQL

📝 DESCRIPCIÓN

Aprende paso a paso cómo crear una App Full Stack Real con FastAPI, Next.js y PostgreSQL, totalmente en contenedores de Podman, usando WebSockets para comunicación en tiempo real y cookies seguras con tokens para autenticación.

🎯 OBJETIVOS DEL VIDEO

Este primer episodio presenta:

  • Visión general del proyecto Full Stack completo
  • Introducción a las tecnologías principales (FastAPI, Next.js, PostgreSQL)
  • Arquitectura del sistema con contenedorización
  • Planteamiento de características clave: WebSockets y autenticación segura

🔑 CONCEPTOS TÉCNICOS PRINCIPALES

1. Stack Tecnológico

  • Backend: FastAPI (Framework Python asíncrono de alto rendimiento)
  • Frontend: Next.js (Framework React con SSR/SSG)
  • Base de Datos: PostgreSQL (Sistema de gestión de base de datos relacional)
  • Contenedorización: Podman (Alternativa a Docker sin daemon root)

2. Características Clave del Proyecto

  • WebSockets: Comunicación bidireccional en tiempo real
  • Autenticación Segura: Tokens JWT almacenados en cookies HttpOnly
  • Arquitectura de Contenedores: Aplicación completamente contenerizada
  • Desarrollo Full Stack: Integración completa frontend-backend

3. Arquitectura del Sistema

┌─────────────────┐     WebSockets/HTTP     ┌──────────────────┐
│   Next.js       │ ←──────────────────────→ │    FastAPI       │
│   Frontend      │     JWT en Cookies      │    Backend       │
│  (Contenedor 1) │                         │  (Contenedor 2)  │
└─────────────────┘                         └──────────────────┘
                                                     ↕
                                            ┌──────────────────┐
                                            │   PostgreSQL     │
                                            │   Base de Datos  │
                                            │  (Contenedor 3)  │
                                            └──────────────────┘

💡 TIPS Y OBSERVACIONES DEL INSTRUCTOR

  • Este es un proyecto real de producción, no un tutorial básico
  • Se seguirán las mejores prácticas profesionales
  • El uso de Podman en lugar de Docker ofrece ventajas en seguridad
  • La serie completa construirá una aplicación funcional paso a paso

🎬 CAPÍTULOS Y TIMESTAMPS

(Nota: Los timestamps específicos no estaban disponibles en los metadatos extraídos)

Contenido estimado:

  • [00:00 - 02:00] Introducción al proyecto Full Stack
  • [02:00 - 04:00] Presentación del stack tecnológico
  • [04:00 - 06:30] Arquitectura y diseño del sistema
  • [06:30 - 08:30] Características principales: WebSockets y autenticación
  • [08:30 - 10:13] Roadmap de la serie y próximos episodios

🔗 CONEXIÓN CON SIGUIENTES EPISODIOS

Este video establece las bases para:

  • Video 2: Implementación y configuración del Backend con FastAPI
  • Video 3: Configuración del Frontend con Next.js
  • Videos posteriores: Refinamiento y características avanzadas

📹 VIDEO 2: FastAPI y Next.js - Preparamos el Backend | Curso App Full Stack #2

📊 INFORMACIÓN GENERAL

  • URL: https://www.youtube.com/watch?v=fNgmKHZ1HJU
  • Duración: 43:31 minutos ⭐ (El episodio más largo de la serie)
  • Vistas: 538 vistas
  • Publicado: Hace 3 meses
  • Me gusta: 33
  • Canal: Código para Principiantes (3.78K suscriptores)
  • Hashtags: #FastAPI #NextJS #PostgreSQL

📝 DESCRIPCIÓN

Continuación del curso Full Stack donde se prepara y configura exhaustivamente el backend con FastAPI, estableciendo la base para la API REST y comunicación en tiempo real.

🎯 OBJETIVOS DEL VIDEO

  • Configurar el entorno de desarrollo del Backend
  • Instalar y configurar FastAPI con todas sus dependencias
  • Establecer la estructura inicial del proyecto
  • Configurar la conexión a PostgreSQL
  • Implementar endpoints básicos de prueba
  • Preparar el contenedor Docker/Podman para el backend

🔑 CONCEPTOS TÉCNICOS PRINCIPALES

1. Instalación y Configuración de FastAPI

Dependencias Principales:

# Instalación de FastAPI y servidor ASGI
pip install fastapi uvicorn[standard]

# Autenticación y seguridad
pip install python-jose[cryptography] passlib[bcrypt]

# Validación de datos
pip install pydantic pydantic[email]

# Base de datos
pip install sqlalchemy psycopg2-binary alembic

# Utilidades
pip install python-multipart

Características de FastAPI exploradas:

  • Servidor ASGI (Uvicorn): Servidor asíncrono de alto rendimiento
  • Validación automática: Uso de Pydantic para validación de tipos
  • Documentación automática: Swagger UI y Redoc generados automáticamente
  • Type hints: Aprovechamiento del tipado estático de Python

2. Estructura Inicial del Proyecto Backend

backend/
├── app/
│   ├── __init__.py
│   ├── main.py              # Punto de entrada de la aplicación
│   ├── config.py            # Configuración y variables de entorno
│   ├── api/
│   │   ├── __init__.py
│   │   └── routes.py        # Definición de rutas/endpoints
│   ├── core/
│   │   ├── __init__.py
│   │   ├── security.py      # JWT, hashing, autenticación
│   │   └── config.py        # Settings centralizados
│   ├── db/
│   │   ├── __init__.py
│   │   └── database.py      # Conexión a PostgreSQL
│   ├── models/
│   │   └── __init__.py      # Modelos SQLAlchemy (próximo video)
│   └── schemas/
│       └── __init__.py      # Esquemas Pydantic (próximo video)
├── requirements.txt
├── Dockerfile
└── .env

3. Configuración de la Base de Datos PostgreSQL

Archivo database.py:

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# URL de conexión a PostgreSQL
DATABASE_URL = "postgresql://user:password@postgres:5432/dbname"

# Crear engine con pool de conexiones
engine = create_engine(
    DATABASE_URL,
    pool_pre_ping=True,  # Verificar conexiones
    pool_size=10,        # Número de conexiones
    max_overflow=20      # Conexiones adicionales máximas
)

# Configurar sesión
SessionLocal = sessionmaker(
    autocommit=False,
    autoflush=False,
    bind=engine
)

# Base para modelos declarativos
Base = declarative_base()

# Dependency para obtener sesión de BD
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

4. Configuración de Variables de Entorno

Archivo .env:

# Database
DATABASE_URL=postgresql://fastapi_user:securepassword@postgres:5432/fullstack_db

# Security
SECRET_KEY=your-super-secret-key-change-in-production
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7

# Application
APP_NAME=FastAPI Full Stack
DEBUG=True
VERSION=1.0.0

Archivo config.py con Pydantic:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    # Database
    DATABASE_URL: str
    
    # Security
    SECRET_KEY: str
    ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
    REFRESH_TOKEN_EXPIRE_DAYS: int = 7
    
    # Application
    APP_NAME: str = "FastAPI Full Stack"
    DEBUG: bool = False
    VERSION: str = "1.0.0"
    
    class Config:
        env_file = ".env"
        case_sensitive = True

settings = Settings()

5. Aplicación Principal FastAPI

Archivo main.py:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import settings
from app.api import routes

# Crear instancia de FastAPI
app = FastAPI(
    title=settings.APP_NAME,
    version=settings.VERSION,
    description="API Full Stack con FastAPI y Next.js",
    docs_url="/docs",
    redoc_url="/redoc"
)

# Configurar CORS para Next.js
origins = [
    "http://localhost:3000",  # Next.js dev
    "http://frontend:3000",   # Next.js container
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Incluir routers
app.include_router(routes.router, prefix="/api/v1")

# Endpoint de health check
@app.get("/health")
async def health_check():
    return {
        "status": "healthy",
        "version": settings.VERSION
    }

# Evento de inicio
@app.on_event("startup")
async def startup_event():
    print(f"🚀 {settings.APP_NAME} iniciado")
    print(f"📚 Documentación en: http://localhost:8000/docs")

# Evento de cierre
@app.on_event("shutdown")
async def shutdown_event():
    print("👋 Cerrando aplicación")

6. Containerización del Backend

Dockerfile para FastAPI:

# Imagen base con Python
FROM python:3.11-slim

# Directorio de trabajo
WORKDIR /app

# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \
    gcc \
    postgresql-client \
    && rm -rf /var/lib/apt/lists/*

# Copiar requirements
COPY requirements.txt .

# Instalar dependencias Python
RUN pip install --no-cache-dir -r requirements.txt

# Copiar código de la aplicación
COPY ./app ./app

# Exponer puerto
EXPOSE 8000

# Comando para ejecutar
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

docker-compose.yml parcial:

version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    container_name: fullstack_postgres
    environment:
      POSTGRES_USER: fastapi_user
      POSTGRES_PASSWORD: securepassword
      POSTGRES_DB: fullstack_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    networks:
      - fullstack_network

  backend:
    build: ./backend
    container_name: fullstack_backend
    env_file:
      - ./backend/.env
    volumes:
      - ./backend/app:/app/app
    ports:
      - "8000:8000"
    depends_on:
      - postgres
    networks:
      - fullstack_network

networks:
  fullstack_network:
    driver: bridge

volumes:
  postgres_data:

7. Comandos y Configuraciones Importantes

Comandos de desarrollo:

# Levantar contenedores con Podman
podman-compose up -d

# Ver logs del backend
podman logs -f fullstack_backend

# Ejecutar migraciones (cuando se configuren)
podman exec fullstack_backend alembic upgrade head

# Acceder al contenedor
podman exec -it fullstack_backend bash

# Detener contenedores
podman-compose down

# Reconstruir imágenes
podman-compose build --no-cache

Ejecutar FastAPI localmente (sin contenedores):

# Modo desarrollo con recarga automática
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

# Modo producción con múltiples workers
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4

💡 TIPS Y OBSERVACIONES DEL INSTRUCTOR

  1. Sobre la estructura del proyecto:

    • Empezar con una estructura simple y escalarla según necesidad
    • Separar claramente las responsabilidades (API, lógica de negocio, datos)
    • Mantener la configuración centralizada
  2. Sobre PostgreSQL:

    • Usar pool de conexiones para mejor rendimiento
    • Configurar pool_pre_ping=True para reconexiones automáticas
    • Siempre cerrar sesiones de base de datos con try/finally
  3. Sobre FastAPI:

    • La documentación automática (Swagger UI) es invaluable para desarrollo
    • Aprovechar la inyección de dependencias para código limpio
    • Usar async/await para operaciones I/O bound
  4. Sobre contenedores:

    • Usar volúmenes para persistencia de datos
    • Configurar networks para comunicación entre contenedores
    • Separar configuraciones de desarrollo y producción
  5. Sobre seguridad:

    • NUNCA commitear archivos .env al repositorio
    • Usar SECRET_KEY fuertes generadas aleatoriamente
    • Configurar CORS apropiadamente según el entorno

🎬 CAPÍTULOS Y TIMESTAMPS ESTIMADOS

  • [00:00 - 05:00] Introducción y repaso del episodio anterior
  • [05:00 - 10:00] Instalación de FastAPI y dependencias
  • [10:00 - 18:00] Configuración de la estructura del proyecto
  • [18:00 - 25:00] Configuración de PostgreSQL y SQLAlchemy
  • [25:00 - 32:00] Creación del archivo main.py y endpoints básicos
  • [32:00 - 38:00] Configuración del Dockerfile y docker-compose
  • [38:00 - 43:31] Pruebas, verificación y conclusiones

🔗 CONEXIÓN CON OTROS EPISODIOS

Depende de:

  • Video 1: Visión general y arquitectura del proyecto

Prepara para:

  • Video 3: Configuración del Frontend con Next.js
  • Video 4: Reorganización profesional del Backend
  • Video 5: Creación de modelos y relaciones de base de datos

📹 VIDEO 3: FastAPI y NextJS - Preparamos el contenedor del Frontend | Curso App Full Stack #3

📊 INFORMACIÓN GENERAL

  • URL: https://www.youtube.com/watch?v=5zkla8UaiwI
  • Duración: 28:14 minutos
  • Vistas: 258 vistas
  • Publicado: Hace 3 meses
  • Me gusta: 18
  • Canal: Código para Principiantes (3.78K suscriptores)
  • Hashtags: #FastAPI #NextJS #PostgreSQL

📝 DESCRIPCIÓN

Episodio dedicado a la configuración completa del contenedor del Frontend con Next.js, estableciendo la base para la interfaz de usuario y la comunicación con el backend FastAPI.

🎯 OBJETIVOS DEL VIDEO

  • Instalar y configurar Next.js
  • Configurar TypeScript para el proyecto
  • Establecer la estructura del proyecto Frontend
  • Configurar TailwindCSS para estilos
  • Implementar componentes UI con shadcn/ui
  • Crear el contenedor Docker/Podman para Next.js
  • Configurar la comunicación con el backend

🔑 CONCEPTOS TÉCNICOS PRINCIPALES

1. Instalación y Configuración de Next.js

Crear proyecto Next.js:

# Crear nuevo proyecto con App Router
npx create-next-app@latest frontend --typescript --tailwind --app

# Opciones seleccionadas:
# ✓ Would you like to use TypeScript? Yes
# ✓ Would you like to use ESLint? Yes
# ✓ Would you like to use Tailwind CSS? Yes
# ✓ Would you like to use `src/` directory? Yes
# ✓ Would you like to use App Router? Yes
# ✓ Would you like to customize the default import alias? No

Dependencias adicionales:

cd frontend

# Cliente HTTP para consumir API
npm install axios

# Manejo de formularios
npm install react-hook-form zod @hookform/resolvers

# Cliente OpenAPI tipado
npm install openapi-fetch openapi-typescript

# Shadcn UI components
npx shadcn-ui@latest init

2. Estructura del Proyecto Frontend

frontend/
├── src/
│   ├── app/
│   │   ├── layout.tsx        # Layout principal
│   │   ├── page.tsx          # Página home
│   │   ├── (auth)/           # Grupo de rutas autenticación
│   │   │   ├── login/
│   │   │   │   └── page.tsx
│   │   │   └── register/
│   │   │       └── page.tsx
│   │   └── (protected)/      # Grupo de rutas protegidas
│   │       └── dashboard/
│   │           └── page.tsx
│   ├── components/
│   │   ├── ui/               # Componentes shadcn
│   │   │   ├── button.tsx
│   │   │   ├── input.tsx
│   │   │   ├── card.tsx
│   │   │   └── ...
│   │   ├── layout/
│   │   │   ├── Header.tsx
│   │   │   ├── Footer.tsx
│   │   │   └── Sidebar.tsx
│   │   └── forms/
│   │       ├── LoginForm.tsx
│   │       └── RegisterForm.tsx
│   ├── lib/
│   │   ├── api.ts           # Cliente API
│   │   ├── auth.ts          # Lógica de autenticación
│   │   └── utils.ts         # Utilidades
│   ├── hooks/
│   │   ├── useAuth.ts
│   │   └── useWebSocket.ts
│   ├── types/
│   │   └── api.ts           # Tipos TypeScript
│   └── styles/
│       └── globals.css
├── public/
│   └── assets/
├── .env.local
├── next.config.js
├── tailwind.config.ts
├── tsconfig.json
├── package.json
└── Dockerfile

3. Configuración de TypeScript

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

4. Configuración de TailwindCSS

tailwind.config.ts:

import type { Config } from "tailwindcss"

const config: Config = {
  darkMode: ["class"],
  content: [
    './pages/**/*.{ts,tsx}',
    './components/**/*.{ts,tsx}',
    './app/**/*.{ts,tsx}',
    './src/**/*.{ts,tsx}',
  ],
  theme: {
    container: {
      center: true,
      padding: "2rem",
      screens: {
        "2xl": "1400px",
      },
    },
    extend: {
      colors: {
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        ring: "hsl(var(--ring))",
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },
        secondary: {
          DEFAULT: "hsl(var(--secondary))",
          foreground: "hsl(var(--secondary-foreground))",
        },
        destructive: {
          DEFAULT: "hsl(var(--destructive))",
          foreground: "hsl(var(--destructive-foreground))",
        },
        muted: {
          DEFAULT: "hsl(var(--muted))",
          foreground: "hsl(var(--muted-foreground))",
        },
        accent: {
          DEFAULT: "hsl(var(--accent))",
          foreground: "hsl(var(--accent-foreground))",
        },
      },
      borderRadius: {
        lg: "var(--radius)",
        md: "calc(var(--radius) - 2px)",
        sm: "calc(var(--radius) - 4px)",
      },
    },
  },
  plugins: [require("tailwindcss-animate")],
}
export default config

5. Cliente API para comunicación con Backend

lib/api.ts:

import axios, { AxiosInstance, AxiosError } from 'axios';

// Configuración base de axios
const apiClient: AxiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1',
  timeout: 10000,
  withCredentials: true, // Para enviar cookies
  headers: {
    'Content-Type': 'application/json',
  },
});

// Interceptor para requests
apiClient.interceptors.request.use(
  (config) => {
    // Agregar token si existe
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// Interceptor para responses
apiClient.interceptors.response.use(
  (response) => response,
  async (error: AxiosError) => {
    if (error.response?.status === 401) {
      // Redirigir a login si no autorizado
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

// Funciones de API
export const api = {
  // Auth
  login: async (username: string, password: string) => {
    const response = await apiClient.post('/auth/login', { username, password });
    return response.data;
  },
  
  register: async (userData: any) => {
    const response = await apiClient.post('/auth/register', userData);
    return response.data;
  },
  
  logout: async () => {
    const response = await apiClient.post('/auth/logout');
    return response.data;
  },
  
  // User
  getCurrentUser: async () => {
    const response = await apiClient.get('/users/me');
    return response.data;
  },
};

export default apiClient;

Variables de entorno (.env.local):

NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
NEXT_PUBLIC_WS_URL=ws://localhost:8000/ws

6. Layout Principal de la Aplicación

app/layout.tsx:

import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'FastAPI + Next.js Full Stack',
  description: 'Aplicación Full Stack con FastAPI y Next.js',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="es">
      <body className={inter.className}>
        <div className="min-h-screen bg-background">
          {children}
        </div>
      </body>
    </html>
  )
}

7. Containerización del Frontend

Dockerfile para Next.js:

# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app

# Copiar package files
COPY package.json package-lock.json* ./
RUN npm ci

# Stage 2: Builder
FROM node:20-alpine AS builder
WORKDIR /app

# Copiar dependencias
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Variables de entorno para build
ENV NEXT_TELEMETRY_DISABLED 1
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL

# Build de la aplicación
RUN npm run build

# Stage 3: Runner
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

# Crear usuario no-root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copiar archivos necesarios
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000
ENV HOSTNAME "0.0.0.0"

CMD ["node", "server.js"]

next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone', // Para Docker
  images: {
    domains: ['localhost'],
  },
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: `${process.env.NEXT_PUBLIC_API_URL}/:path*`,
      },
    ];
  },
}

module.exports = nextConfig

docker-compose.yml actualizado:

version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    container_name: fullstack_postgres
    environment:
      POSTGRES_USER: fastapi_user
      POSTGRES_PASSWORD: securepassword
      POSTGRES_DB: fullstack_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    networks:
      - fullstack_network

  backend:
    build: ./backend
    container_name: fullstack_backend
    env_file:
      - ./backend/.env
    volumes:
      - ./backend/app:/app/app
    ports:
      - "8000:8000"
    depends_on:
      - postgres
    networks:
      - fullstack_network

  frontend:
    build: 
      context: ./frontend
      args:
        NEXT_PUBLIC_API_URL: http://backend:8000/api/v1
    container_name: fullstack_frontend
    environment:
      - NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
      - NEXT_PUBLIC_WS_URL=ws://localhost:8000/ws
    ports:
      - "3000:3000"
    depends_on:
      - backend
    networks:
      - fullstack_network
    volumes:
      - ./frontend/src:/app/src
      - ./frontend/public:/app/public

networks:
  fullstack_network:
    driver: bridge

volumes:
  postgres_data:

8. Comandos Importantes para el Frontend

Desarrollo local:

# Instalar dependencias
npm install

# Modo desarrollo
npm run dev

# Build de producción
npm run build

# Ejecutar build
npm start

# Linter
npm run lint

Con contenedores:

# Build del contenedor frontend
podman build -t fullstack-frontend ./frontend

# Ejecutar solo frontend
podman run -p 3000:3000 fullstack-frontend

# Levantar stack completo
podman-compose up -d

# Ver logs del frontend
podman logs -f fullstack_frontend

💡 TIPS Y OBSERVACIONES DEL INSTRUCTOR

  1. Sobre Next.js:

    • App Router es la nueva forma recomendada (vs Pages Router)
    • Server Components mejoran el rendimiento inicial
    • Usar Server Actions para formularios simples
  2. Sobre TypeScript:

    • Aprovechar el tipado fuerte para prevenir errores
    • Generar tipos automáticamente desde OpenAPI del backend
    • Usar interfaces para estructuras de datos complejas
  3. Sobre Tailwind y shadcn/ui:

    • shadcn/ui provee componentes reutilizables y personalizables
    • Mantener consistencia en el diseño usando variables CSS
    • Aprovechar las utilidades de Tailwind para desarrollo rápido
  4. Sobre la comunicación Frontend-Backend:

    • Usar withCredentials: true para cookies HttpOnly
    • Configurar CORS apropiadamente en ambos lados
    • Manejar errores de red con interceptors de axios
  5. Sobre contenedores:

    • Multi-stage builds reducen el tamaño de la imagen final
    • Usar standalone output para optimizar el contenedor
    • Separar variables de entorno para build y runtime

🎬 CAPÍTULOS Y TIMESTAMPS ESTIMADOS

  • [00:00 - 03:00] Introducción y repaso
  • [03:00 - 08:00] Instalación de Next.js y configuración inicial
  • [08:00 - 12:00] Configuración de TypeScript y TailwindCSS
  • [12:00 - 18:00] Estructura del proyecto y componentes básicos
  • [18:00 - 22:00] Cliente API y comunicación con backend
  • [22:00 - 26:00] Containerización con Docker/Podman
  • [26:00 - 28:14] Pruebas y conclusiones

🔗 CONEXIÓN CON OTROS EPISODIOS

Depende de:

  • Video 1: Arquitectura general
  • Video 2: Backend configurado y funcionando

Prepara para:

  • Video 4: Refinamiento del Backend
  • Videos futuros: Implementación de características (autenticación, WebSockets)

📹 VIDEO 4: FastAPI y NextJS - Reorganizamos profesionalmente el Backend | Curso App Full Stack #4

📊 INFORMACIÓN GENERAL

  • URL: https://www.youtube.com/watch?v=4c1Tw2rb5cE
  • Duración: 16:14 minutos
  • Vistas: 142 vistas
  • Publicado: Hace 2 meses
  • Me gusta: 10
  • Canal: Código para Principiantes (3.78K suscriptores)
  • Hashtags: #FastAPI #NextJS #PostgreSQL

📝 DESCRIPCIÓN

Episodio enfocado en la refactorización profesional del backend, aplicando mejores prácticas de arquitectura y organización del código para lograr un proyecto escalable y mantenible.

🎯 OBJETIVOS DEL VIDEO

  • Refactorizar la estructura del proyecto backend
  • Implementar arquitectura en capas
  • Separar responsabilidades (routers, services, repositories)
  • Aplicar patrones de diseño profesionales
  • Mejorar la inyección de dependencias
  • Preparar el código para escalabilidad

🔑 CONCEPTOS TÉCNICOS PRINCIPALES

1. Arquitectura en Capas Profesional

La nueva estructura implementa una arquitectura limpia con separación clara de responsabilidades:

backend/
├── app/
│   ├── __init__.py
│   ├── main.py
│   │
│   ├── api/                  # CAPA DE PRESENTACIÓN
│   │   ├── __init__.py
│   │   ├── deps.py          # Dependencias compartidas
│   │   └── v1/
│   │       ├── __init__.py
│   │       ├── router.py    # Router principal v1
│   │       └── endpoints/
│   │           ├── __init__.py
│   │           ├── auth.py
│   │           ├── users.py
│   │           └── websocket.py
│   │
│   ├── core/                 # CONFIGURACIÓN CENTRAL
│   │   ├── __init__.py
│   │   ├── config.py        # Settings con Pydantic
│   │   ├── security.py      # JWT, hashing, tokens
│   │   └── exceptions.py    # Excepciones personalizadas
│   │
│   ├── db/                   # CAPA DE PERSISTENCIA
│   │   ├── __init__.py
│   │   ├── base.py          # Base class imports
│   │   ├── session.py       # Session management
│   │   └── init_db.py       # DB initialization
│   │
│   ├── models/               # MODELOS ORM (SQLAlchemy)
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── user.py
│   │   ├── project.py
│   │   └── task.py
│   │
│   ├── schemas/              # ESQUEMAS PYDANTIC
│   │   ├── __init__.py
│   │   ├── user.py
│   │   ├── project.py
│   │   ├── task.py
│   │   └── token.py
│   │
│   ├── services/             # CAPA DE LÓGICA DE NEGOCIO
│   │   ├── __init__.py
│   │   ├── auth_service.py
│   │   ├── user_service.py
│   │   └── websocket_service.py
│   │
│   ├── repositories/         # CAPA DE ACCESO A DATOS
│   │   ├── __init__.py
│   │   ├── base.py          # CRUD genérico
│   │   ├── user_repo.py
│   │   └── project_repo.py
│   │
│   └── utils/                # UTILIDADES
│       ├── __init__.py
│       └── helpers.py
│
├── tests/                    # TESTS
│   ├── __init__.py
│   ├── conftest.py
│   ├── api/
│   ├── services/
│   └── repositories/
│
├── alembic/                  # MIGRACIONES
│   ├── versions/
│   └── env.py
│
├── .env
├── .env.example
├── requirements.txt
├── alembic.ini
└── Dockerfile

2. Patrón Repository para Acceso a Datos

repositories/base.py - Repositorio genérico con operaciones CRUD:

from typing import TypeVar, Generic, Type, Optional, List
from sqlalchemy.orm import Session
from sqlalchemy import select
from app.db.base import Base

ModelType = TypeVar("ModelType", bound=Base)

class BaseRepository(Generic[ModelType]):
    """
    Repositorio base con operaciones CRUD genéricas
    """
    def __init__(self, model: Type[ModelType]):
        self.model = model
    
    def get(self, db: Session, id: int) -> Optional[ModelType]:
        """Obtener por ID"""
        return db.query(self.model).filter(self.model.id == id).first()
    
    def get_multi(
        self, 
        db: Session, 
        *, 
        skip: int = 0, 
        limit: int = 100
    ) -> List[ModelType]:
        """Obtener múltiples registros"""
        return db.query(self.model).offset(skip).limit(limit).all()
    
    def create(self, db: Session, *, obj_in: dict) -> ModelType:
        """Crear nuevo registro"""
        db_obj = self.model(**obj_in)
        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj
    
    def update(
        self, 
        db: Session, 
        *, 
        db_obj: ModelType, 
        obj_in: dict
    ) -> ModelType:
        """Actualizar registro existente"""
        for field, value in obj_in.items():
            setattr(db_obj, field, value)
        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj
    
    def delete(self, db: Session, *, id: int) -> Optional[ModelType]:
        """Eliminar registro"""
        obj = db.query(self.model).get(id)
        if obj:
            db.delete(obj)
            db.commit()
        return obj

repositories/user_repo.py - Repositorio específico de usuarios:

from typing import Optional
from sqlalchemy.orm import Session
from app.models.user import User
from app.repositories.base import BaseRepository

class UserRepository(BaseRepository[User]):
    """
    Repositorio para operaciones de usuarios
    """
    def __init__(self):
        super().__init__(User)
    
    def get_by_email(self, db: Session, *, email: str) -> Optional[User]:
        """Obtener usuario por email"""
        return db.query(User).filter(User.email == email).first()
    
    def get_by_username(self, db: Session, *, username: str) -> Optional[User]:
        """Obtener usuario por username"""
        return db.query(User).filter(User.username == username).first()
    
    def is_active(self, user: User) -> bool:
        """Verificar si usuario está activo"""
        return user.is_active
    
    def is_superuser(self, user: User) -> bool:
        """Verificar si usuario es superusuario"""
        return user.is_superuser

# Instancia global del repositorio
user_repository = UserRepository()

3. Capa de Servicios (Lógica de Negocio)

services/user_service.py:

from typing import Optional, List
from sqlalchemy.orm import Session
from app.repositories.user_repo import user_repository
from app.schemas.user import UserCreate, UserUpdate
from app.models.user import User
from app.core.security import get_password_hash, verify_password
from app.core.exceptions import UserAlreadyExistsException, UserNotFoundException

class UserService:
    """
    Servicio con lógica de negocio de usuarios
    """
    
    def __init__(self):
        self.repo = user_repository
    
    def create_user(self, db: Session, user_in: UserCreate) -> User:
        """
        Crear nuevo usuario con validaciones de negocio
        """
        # Validar que no exista el email
        existing_user = self.repo.get_by_email(db, email=user_in.email)
        if existing_user:
            raise UserAlreadyExistsException("Email ya registrado")
        
        # Validar que no exista el username
        existing_username = self.repo.get_by_username(db, username=user_in.username)
        if existing_username:
            raise UserAlreadyExistsException("Username ya existe")
        
        # Hash de la contraseña
        hashed_password = get_password_hash(user_in.password)
        
        # Crear usuario
        user_data = user_in.dict(exclude={'password'})
        user_data['hashed_password'] = hashed_password
        
        return self.repo.create(db, obj_in=user_data)
    
    def authenticate(
        self, 
        db: Session, 
        *, 
        email: str, 
        password: str
    ) -> Optional[User]:
        """
        Autenticar usuario
        """
        user = self.repo.get_by_email(db, email=email)
        if not user:
            return None
        if not verify_password(password, user.hashed_password):
            return None
        if not self.repo.is_active(user):
            return None
        return user
    
    def get_user(self, db: Session, user_id: int) -> User:
        """
        Obtener usuario por ID
        """
        user = self.repo.get(db, id=user_id)
        if not user:
            raise UserNotFoundException(f"Usuario {user_id} no encontrado")
        return user
    
    def get_users(
        self, 
        db: Session, 
        *, 
        skip: int = 0, 
        limit: int = 100
    ) -> List[User]:
        """
        Obtener lista de usuarios
        """
        return self.repo.get_multi(db, skip=skip, limit=limit)
    
    def update_user(
        self, 
        db: Session, 
        *, 
        user_id: int, 
        user_in: UserUpdate
    ) -> User:
        """
        Actualizar usuario
        """
        user = self.get_user(db, user_id)
        update_data = user_in.dict(exclude_unset=True)
        
        if 'password' in update_data:
            update_data['hashed_password'] = get_password_hash(update_data['password'])
            del update_data['password']
        
        return self.repo.update(db, db_obj=user, obj_in=update_data)

# Instancia global del servicio
user_service = UserService()

4. Excepciones Personalizadas

core/exceptions.py:

from fastapi import HTTPException, status

class BaseAPIException(HTTPException):
    """Excepción base para la API"""
    def __init__(self, detail: str, status_code: int = status.HTTP_400_BAD_REQUEST):
        super().__init__(status_code=status_code, detail=detail)

class UserAlreadyExistsException(BaseAPIException):
    """Usuario ya existe"""
    def __init__(self, detail: str = "El usuario ya existe"):
        super().__init__(detail=detail, status_code=status.HTTP_409_CONFLICT)

class UserNotFoundException(BaseAPIException):
    """Usuario no encontrado"""
    def __init__(self, detail: str = "Usuario no encontrado"):
        super().__init__(detail=detail, status_code=status.HTTP_404_NOT_FOUND)

class InvalidCredentialsException(BaseAPIException):
    """Credenciales inválidas"""
    def __init__(self, detail: str = "Credenciales inválidas"):
        super().__init__(detail=detail, status_code=status.HTTP_401_UNAUTHORIZED)

class InsufficientPermissionsException(BaseAPIException):
    """Permisos insuficientes"""
    def __init__(self, detail: str = "Permisos insuficientes"):
        super().__init__(detail=detail, status_code=status.HTTP_403_FORBIDDEN)

5. Dependencias Compartidas

api/deps.py:

from typing import Generator, Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from sqlalchemy.orm import Session
from app.core.config import settings
from app.db.session import SessionLocal
from app.models.user import User
from app.services.user_service import user_service

# OAuth2 scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")

def get_db() -> Generator:
    """
    Dependency para obtener sesión de base de datos
    """
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

async def get_current_user(
    db: Session = Depends(get_db),
    token: str = Depends(oauth2_scheme)
) -> User:
    """
    Dependency para obtener usuario actual desde JWT
    """
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="No se pudo validar las credenciales",
        headers={"WWW-Authenticate": "Bearer"},
    )
    
    try:
        payload = jwt.decode(
            token, 
            settings.SECRET_KEY, 
            algorithms=[settings.ALGORITHM]
        )
        user_id: str = payload.get("sub")
        if user_id is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    
    user = user_service.get_user(db, user_id=int(user_id))
    if user is None:
        raise credentials_exception
    
    return user

async def get_current_active_user(
    current_user: User = Depends(get_current_user),
) -> User:
    """
    Dependency para verificar que usuario esté activo
    """
    if not current_user.is_active:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST, 
            detail="Usuario inactivo"
        )
    return current_user

async def get_current_active_superuser(
    current_user: User = Depends(get_current_user),
) -> User:
    """
    Dependency para verificar que usuario sea superusuario
    """
    if not current_user.is_superuser:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Permisos insuficientes"
        )
    return current_user

6. Endpoints Refactorizados

api/v1/endpoints/users.py:

from typing import List
from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from app.api import deps
from app.schemas.user import User as UserSchema, UserCreate, UserUpdate
from app.services.user_service import user_service
from app.models.user import User

router = APIRouter()

@router.get("/", response_model=List[UserSchema])
def list_users(
    db: Session = Depends(deps.get_db),
    skip: int = 0,
    limit: int = 100,
    current_user: User = Depends(deps.get_current_active_superuser)
):
    """
    Listar usuarios (solo superusuarios)
    """
    users = user_service.get_users(db, skip=skip, limit=limit)
    return users

@router.get("/me", response_model=UserSchema)
def read_user_me(
    current_user: User = Depends(deps.get_current_active_user),
):
    """
    Obtener usuario actual
    """
    return current_user

@router.get("/{user_id}", response_model=UserSchema)
def read_user(
    user_id: int,
    db: Session = Depends(deps.get_db),
    current_user: User = Depends(deps.get_current_active_user),
):
    """
    Obtener usuario específico
    """
    return user_service.get_user(db, user_id=user_id)

@router.put("/{user_id}", response_model=UserSchema)
def update_user(
    user_id: int,
    user_in: UserUpdate,
    db: Session = Depends(deps.get_db),
    current_user: User = Depends(deps.get_current_active_superuser),
):
    """
    Actualizar usuario (solo superusuarios)
    """
    return user_service.update_user(db, user_id=user_id, user_in=user_in)

7. Router Principal Versionado

api/v1/router.py:

from fastapi import APIRouter
from app.api.v1.endpoints import auth, users, websocket

api_router = APIRouter()

# Incluir todos los routers de endpoints
api_router.include_router(
    auth.router, 
    prefix="/auth", 
    tags=["authentication"]
)
api_router.include_router(
    users.router, 
    prefix="/users", 
    tags=["users"]
)
api_router.include_router(
    websocket.router, 
    prefix="/ws", 
    tags=["websocket"]
)

main.py actualizado:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import settings
from app.api.v1.router import api_router

app = FastAPI(
    title=settings.APP_NAME,
    version=settings.VERSION,
    description="API Full Stack Profesional con FastAPI y Next.js",
    openapi_url=f"/api/v1/openapi.json",
    docs_url="/api/docs",
    redoc_url="/api/redoc"
)

# CORS
origins = [
    "http://localhost:3000",
    "http://frontend:3000",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Incluir router principal
app.include_router(api_router, prefix="/api/v1")

@app.get("/health")
async def health_check():
    return {
        "status": "healthy",
        "version": settings.VERSION,
        "environment": settings.ENVIRONMENT
    }

💡 TIPS Y OBSERVACIONES DEL INSTRUCTOR

  1. Sobre Arquitectura en Capas:

    • Cada capa tiene una responsabilidad única y clara
    • Facilita el testing al poder mockear cada capa independientemente
    • Permite cambiar implementaciones sin afectar otras capas
  2. Sobre el Patrón Repository:

    • Abstrae la lógica de acceso a datos
    • Facilita el cambio de ORM o base de datos en el futuro
    • Permite reutilizar operaciones CRUD comunes
  3. Sobre la Capa de Servicios:

    • Contiene toda la lógica de negocio
    • Los endpoints solo orquestan llamadas a servicios
    • Facilita la reutilización de lógica en diferentes endpoints
  4. Sobre Excepciones Personalizadas:

    • Proporciona mensajes de error consistentes
    • Facilita el manejo centralizado de errores
    • Mejora la experiencia del cliente de la API
  5. Sobre Dependencias:

    • FastAPI maneja automáticamente la inyección de dependencias
    • Permite composición de dependencias (una depende de otra)
    • Facilita el testing con dependencias mockeadas

🎬 CAPÍTULOS Y TIMESTAMPS ESTIMADOS

  • [00:00 - 02:00] Introducción y problemas de la estructura anterior
  • [02:00 - 05:00] Diseño de la nueva arquitectura en capas
  • [05:00 - 08:00] Implementación del patrón Repository
  • [08:00 - 11:00] Creación de la capa de servicios
  • [11:00 - 14:00] Refactorización de endpoints
  • [14:00 - 16:14] Testing y conclusiones

🔗 CONEXIÓN CON OTROS EPISODIOS

Depende de:

  • Video 2: Estructura inicial del backend
  • Video 3: Frontend configurado

Prepara para:

  • Video 5: Creación de modelos complejos con relaciones
  • Videos futuros: Implementación de características avanzadas

Mejoras introducidas:

  • Arquitectura escalable y mantenible
  • Separación clara de responsabilidades
  • Código más testeable
  • Base sólida para crecer el proyecto

📹 VIDEO 5: FastAPI y NextJS - Creamos los modelos con sus relaciones | Curso App Full Stack #5

📊 INFORMACIÓN GENERAL

  • URL: https://www.youtube.com/watch?v=z1Q8dcP5jWI
  • Duración: 22:06 minutos
  • Vistas: 221 vistas
  • Publicado: Hace 2 meses
  • Me gusta: 15
  • Canal: Código para Principiantes (3.78K suscriptores)
  • Hashtags: #FastAPI #NextJS #PostgreSQL

📝 DESCRIPCIÓN

Episodio dedicado al modelado completo de la base de datos, implementando modelos SQLAlchemy con relaciones complejas entre entidades, aplicando las mejores prácticas de diseño de bases de datos relacionales.

🎯 OBJETIVOS DEL VIDEO

  • Diseñar el esquema completo de la base de datos
  • Crear modelos SQLAlchemy con relaciones
  • Implementar diferentes tipos de relaciones (1:1, 1:N, N:M)
  • Configurar cascadas y comportamientos de eliminación
  • Crear migraciones con Alembic
  • Implementar esquemas Pydantic correspondientes

🔑 CONCEPTOS TÉCNICOS PRINCIPALES

1. Diseño del Esquema de Base de Datos

Entidades principales del sistema:

┌──────────────┐
│    User      │
│──────────────│
│ id (PK)      │
│ username     │
│ email        │
│ password_hash│
│ is_active    │
│ is_superuser │
│ created_at   │
│ updated_at   │
└──────────────┘
       │
       │ 1:N (un usuario, muchos proyectos)
       │
       ▼
┌──────────────┐
│   Project    │
│──────────────│
│ id (PK)      │
│ name         │
│ description  │
│ owner_id (FK)│
│ is_active    │
│ created_at   │
│ updated_at   │
└──────────────┘
       │
       │ 1:N (un proyecto, muchas tareas)
       │
       ▼
┌──────────────┐
│    Task      │
│──────────────│
│ id (PK)      │
│ title        │
│ description  │
│ status       │
│ priority     │
│ project_id   │
│ assigned_to  │
│ created_at   │
│ updated_at   │
│ due_date     │
└──────────────┘
       │
       │ N:M (muchas tareas, muchos tags)
       │
       ▼
┌──────────────┐       ┌──────────────┐
│   TaskTag    │       │     Tag      │
│──────────────│       │──────────────│
│ task_id (FK) │◄──────│ id (PK)      │
│ tag_id (FK)  │       │ name         │
└──────────────┘       │ color        │
                       └──────────────┘

2. Modelo Base con Timestamps

models/base.py:

from datetime import datetime
from sqlalchemy import Column, Integer, DateTime
from sqlalchemy.ext.declarative import declared_attr
from app.db.base import Base

class TimestampMixin:
    """
    Mixin para agregar timestamps automáticos
    """
    created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
    updated_at = Column(
        DateTime, 
        default=datetime.utcnow, 
        onupdate=datetime.utcnow, 
        nullable=False
    )

class BaseModel(Base, TimestampMixin):
    """
    Clase base para todos los modelos
    """
    __abstract__ = True
    
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    
    @declared_attr
    def __tablename__(cls) -> str:
        """Generar nombre de tabla automáticamente"""
        return cls.__name__.lower()

3. Modelo de Usuario

models/user.py:

from sqlalchemy import Boolean, Column, String
from sqlalchemy.orm import relationship
from app.models.base import BaseModel

class User(BaseModel):
    """
    Modelo de usuario del sistema
    """
    __tablename__ = "users"
    
    # Campos básicos
    username = Column(String(50), unique=True, index=True, nullable=False)
    email = Column(String(255), unique=True, index=True, nullable=False)
    full_name = Column(String(100), nullable=True)
    hashed_password = Column(String(255), nullable=False)
    
    # Estados
    is_active = Column(Boolean, default=True)
    is_superuser = Column(Boolean, default=False)
    is_verified = Column(Boolean, default=False)
    
    # Relaciones
    projects = relationship(
        "Project", 
        back_populates="owner",
        cascade="all, delete-orphan",  # Eliminar proyectos si se elimina usuario
        lazy="dynamic"  # Carga bajo demanda
    )
    
    assigned_tasks = relationship(
        "Task",
        back_populates="assignee",
        foreign_keys="[Task.assigned_to_id]",
        lazy="dynamic"
    )
    
    def __repr__(self):
        return f"<User(id={self.id}, username='{self.username}', email='{self.email}')>"

Esquema Pydantic correspondiente (schemas/user.py):

from typing import Optional
from datetime import datetime
from pydantic import BaseModel, EmailStr, Field

class UserBase(BaseModel):
    """Schema base de usuario"""
    email: EmailStr
    username: str = Field(..., min_length=3, max_length=50)
    full_name: Optional[str] = Field(None, max_length=100)
    is_active: bool = True

class UserCreate(UserBase):
    """Schema para crear usuario"""
    password: str = Field(..., min_length=8, max_length=100)

class UserUpdate(BaseModel):
    """Schema para actualizar usuario"""
    email: Optional[EmailStr] = None
    username: Optional[str] = Field(None, min_length=3, max_length=50)
    full_name: Optional[str] = Field(None, max_length=100)
    password: Optional[str] = Field(None, min_length=8, max_length=100)
    is_active: Optional[bool] = None

class User(UserBase):
    """Schema de respuesta de usuario"""
    id: int
    is_superuser: bool
    is_verified: bool
    created_at: datetime
    updated_at: datetime
    
    class Config:
        from_attributes = True  # Antes orm_mode = True

class UserInDB(User):
    """Schema de usuario en DB (incluye hash)"""
    hashed_password: str

4. Modelo de Proyecto con Relación 1:N

models/project.py:

from sqlalchemy import Boolean, Column, String, Text, Integer, ForeignKey
from sqlalchemy.orm import relationship
from app.models.base import BaseModel

class Project(BaseModel):
    """
    Modelo de proyecto
    Relación 1:N con User (un usuario, muchos proyectos)
    Relación 1:N con Task (un proyecto, muchas tareas)
    """
    __tablename__ = "projects"
    
    # Campos básicos
    name = Column(String(100), nullable=False, index=True)
    description = Column(Text, nullable=True)
    is_active = Column(Boolean, default=True)
    
    # Foreign Keys
    owner_id = Column(
        Integer, 
        ForeignKey("users.id", ondelete="CASCADE"),
        nullable=False,
        index=True
    )
    
    # Relaciones
    owner = relationship(
        "User", 
        back_populates="projects"
    )
    
    tasks = relationship(
        "Task",
        back_populates="project",
        cascade="all, delete-orphan",  # Eliminar tareas si se elimina proyecto
        lazy="dynamic"
    )
    
    def __repr__(self):
        return f"<Project(id={self.id}, name='{self.name}', owner_id={self.owner_id})>"

Esquema Pydantic (schemas/project.py):

from typing import Optional, List
from datetime import datetime
from pydantic import BaseModel, Field

class ProjectBase(BaseModel):
    """Schema base de proyecto"""
    name: str = Field(..., min_length=1, max_length=100)
    description: Optional[str] = None
    is_active: bool = True

class ProjectCreate(ProjectBase):
    """Schema para crear proyecto"""
    pass

class ProjectUpdate(BaseModel):
    """Schema para actualizar proyecto"""
    name: Optional[str] = Field(None, min_length=1, max_length=100)
    description: Optional[str] = None
    is_active: Optional[bool] = None

class Project(ProjectBase):
    """Schema de respuesta de proyecto"""
    id: int
    owner_id: int
    created_at: datetime
    updated_at: datetime
    
    class Config:
        from_attributes = True

class ProjectWithOwner(Project):
    """Schema de proyecto con información del owner"""
    from app.schemas.user import User
    owner: User

class ProjectWithTasks(Project):
    """Schema de proyecto con tareas"""
    from app.schemas.task import Task
    tasks: List[Task] = []

5. Modelo de Tarea con Relaciones Múltiples

models/task.py:

from sqlalchemy import (
    Column, String, Text, Integer, 
    ForeignKey, Enum, Date
)
from sqlalchemy.orm import relationship
import enum
from app.models.base import BaseModel

class TaskStatus(str, enum.Enum):
    """Estados posibles de una tarea"""
    TODO = "todo"
    IN_PROGRESS = "in_progress"
    IN_REVIEW = "in_review"
    DONE = "done"
    CANCELLED = "cancelled"

class TaskPriority(str, enum.Enum):
    """Prioridades de tarea"""
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    URGENT = "urgent"

class Task(BaseModel):
    """
    Modelo de tarea
    """
    __tablename__ = "tasks"
    
    # Campos básicos
    title = Column(String(200), nullable=False, index=True)
    description = Column(Text, nullable=True)
    status = Column(
        Enum(TaskStatus), 
        default=TaskStatus.TODO, 
        nullable=False,
        index=True
    )
    priority = Column(
        Enum(TaskPriority), 
        default=TaskPriority.MEDIUM, 
        nullable=False
    )
    due_date = Column(Date, nullable=True)
    
    # Foreign Keys
    project_id = Column(
        Integer,
        ForeignKey("projects.id", ondelete="CASCADE"),
        nullable=False,
        index=True
    )
    
    assigned_to_id = Column(
        Integer,
        ForeignKey("users.id", ondelete="SET NULL"),
        nullable=True,
        index=True
    )
    
    # Relaciones
    project = relationship(
        "Project",
        back_populates="tasks"
    )
    
    assignee = relationship(
        "User",
        back_populates="assigned_tasks",
        foreign_keys=[assigned_to_id]
    )
    
    tags = relationship(
        "Tag",
        secondary="task_tags",  # Tabla intermedia para N:M
        back_populates="tasks",
        lazy="dynamic"
    )
    
    def __repr__(self):
        return f"<Task(id={self.id}, title='{self.title}', status='{self.status}')>"

Esquema Pydantic (schemas/task.py):

from typing import Optional, List
from datetime import datetime, date
from pydantic import BaseModel, Field
from app.models.task import TaskStatus, TaskPriority

class TaskBase(BaseModel):
    """Schema base de tarea"""
    title: str = Field(..., min_length=1, max_length=200)
    description: Optional[str] = None
    status: TaskStatus = TaskStatus.TODO
    priority: TaskPriority = TaskPriority.MEDIUM
    due_date: Optional[date] = None

class TaskCreate(TaskBase):
    """Schema para crear tarea"""
    project_id: int
    assigned_to_id: Optional[int] = None

class TaskUpdate(BaseModel):
    """Schema para actualizar tarea"""
    title: Optional[str] = Field(None, min_length=1, max_length=200)
    description: Optional[str] = None
    status: Optional[TaskStatus] = None
    priority: Optional[TaskPriority] = None
    due_date: Optional[date] = None
    assigned_to_id: Optional[int] = None

class Task(TaskBase):
    """Schema de respuesta de tarea"""
    id: int
    project_id: int
    assigned_to_id: Optional[int]
    created_at: datetime
    updated_at: datetime
    
    class Config:
        from_attributes = True

class TaskWithRelations(Task):
    """Schema de tarea con relaciones"""
    from app.schemas.user import User
    from app.schemas.project import Project
    from app.schemas.tag import Tag
    
    project: Project
    assignee: Optional[User] = None
    tags: List[Tag] = []

6. Relación N:M con Tags

models/tag.py:

from sqlalchemy import Column, String, Integer, ForeignKey, Table
from sqlalchemy.orm import relationship
from app.models.base import BaseModel, Base

# Tabla intermedia para relación N:M entre Task y Tag
task_tags = Table(
    'task_tags',
    Base.metadata,
    Column('task_id', Integer, ForeignKey('tasks.id', ondelete="CASCADE"), primary_key=True),
    Column('tag_id', Integer, ForeignKey('tags.id', ondelete="CASCADE"), primary_key=True)
)

class Tag(BaseModel):
    """
    Modelo de etiqueta/tag
    Relación N:M con Task
    """
    __tablename__ = "tags"
    
    name = Column(String(50), unique=True, nullable=False, index=True)
    color = Column(String(7), default="#808080")  # Color hexadecimal
    
    # Relación N:M con tasks
    tasks = relationship(
        "Task",
        secondary=task_tags,
        back_populates="tags",
        lazy="dynamic"
    )
    
    def __repr__(self):
        return f"<Tag(id={self.id}, name='{self.name}')>"

Esquema Pydantic (schemas/tag.py):

from typing import Optional
from datetime import datetime
from pydantic import BaseModel, Field

class TagBase(BaseModel):
    """Schema base de tag"""
    name: str = Field(..., min_length=1, max_length=50)
    color: str = Field(default="#808080", regex=r"^#[0-9A-Fa-f]{6}$")

class TagCreate(TagBase):
    """Schema para crear tag"""
    pass

class TagUpdate(BaseModel):
    """Schema para actualizar tag"""
    name: Optional[str] = Field(None, min_length=1, max_length=50)
    color: Optional[str] = Field(None, regex=r"^#[0-9A-Fa-f]{6}$")

class Tag(TagBase):
    """Schema de respuesta de tag"""
    id: int
    created_at: datetime
    updated_at: datetime
    
    class Config:
        from_attributes = True

7. Configuración de Migraciones con Alembic

Inicializar Alembic:

# Inicializar Alembic
alembic init alembic

# Esto crea:
# - alembic/
# - alembic.ini

Configurar alembic/env.py:

from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context
from app.core.config import settings
from app.db.base import Base
# Importar todos los modelos para que Alembic los detecte
from app.models import user, project, task, tag

# Configuración de Alembic
config = context.config

# Configurar URL de la base de datos
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)

# Interpretar config file para logging
if config.config_file_name is not None:
    fileConfig(config.config_file_name)

# Metadata para autogenerar migraciones
target_metadata = Base.metadata

def run_migrations_offline() -> None:
    """Run migrations in 'offline' mode."""
    url = config.get_main_option("sqlalchemy.url")
    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        dialect_opts={"paramstyle": "named"},
    )

    with context.begin_transaction():
        context.run_migrations()

def run_migrations_online() -> None:
    """Run migrations in 'online' mode."""
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )

    with connectable.connect() as connection:
        context.configure(
            connection=connection, 
            target_metadata=target_metadata
        )

        with context.begin_transaction():
            context.run_migrations()

if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()

Comandos de Alembic:

# Crear migración automática
alembic revision --autogenerate -m "Create users, projects, tasks and tags tables"

# Aplicar migraciones
alembic upgrade head

# Ver historial de migraciones
alembic history

# Revertir última migración
alembic downgrade -1

# Ver SQL sin ejecutar
alembic upgrade head --sql

# Crear migración vacía
alembic revision -m "Add custom changes"

8. Ejemplo de Migración Generada

alembic/versions/001_create_initial_tables.py:

"""Create users, projects, tasks and tags tables

Revision ID: 001
Revises: 
Create Date: 2024-01-XX
"""
from alembic import op
import sqlalchemy as sa

# revision identifiers
revision = '001'
down_revision = None
branch_labels = None
depends_on = None

def upgrade() -> None:
    # Crear tabla users
    op.create_table(
        'users',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('username', sa.String(length=50), nullable=False),
        sa.Column('email', sa.String(length=255), nullable=False),
        sa.Column('full_name', sa.String(length=100), nullable=True),
        sa.Column('hashed_password', sa.String(length=255), nullable=False),
        sa.Column('is_active', sa.Boolean(), nullable=True),
        sa.Column('is_superuser', sa.Boolean(), nullable=True),
        sa.Column('is_verified', sa.Boolean(), nullable=True),
        sa.Column('created_at', sa.DateTime(), nullable=False),
        sa.Column('updated_at', sa.DateTime(), nullable=False),
        sa.PrimaryKeyConstraint('id')
    )
    op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
    op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
    op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
    
    # Crear tabla projects
    op.create_table(
        'projects',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('name', sa.String(length=100), nullable=False),
        sa.Column('description', sa.Text(), nullable=True),
        sa.Column('is_active', sa.Boolean(), nullable=True),
        sa.Column('owner_id', sa.Integer(), nullable=False),
        sa.Column('created_at', sa.DateTime(), nullable=False),
        sa.Column('updated_at', sa.DateTime(), nullable=False),
        sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ondelete='CASCADE'),
        sa.PrimaryKeyConstraint('id')
    )
    op.create_index(op.f('ix_projects_id'), 'projects', ['id'], unique=False)
    op.create_index(op.f('ix_projects_name'), 'projects', ['name'], unique=False)
    op.create_index(op.f('ix_projects_owner_id'), 'projects', ['owner_id'], unique=False)
    
    # Crear tabla tags
    op.create_table(
        'tags',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('name', sa.String(length=50), nullable=False),
        sa.Column('color', sa.String(length=7), nullable=True),
        sa.Column('created_at', sa.DateTime(), nullable=False),
        sa.Column('updated_at', sa.DateTime(), nullable=False),
        sa.PrimaryKeyConstraint('id'),
        sa.UniqueConstraint('name')
    )
    op.create_index(op.f('ix_tags_id'), 'tags', ['id'], unique=False)
    op.create_index(op.f('ix_tags_name'), 'tags', ['name'], unique=True)
    
    # Crear tabla tasks
    op.create_table(
        'tasks',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('title', sa.String(length=200), nullable=False),
        sa.Column('description', sa.Text(), nullable=True),
        sa.Column('status', sa.Enum('TODO', 'IN_PROGRESS', 'IN_REVIEW', 'DONE', 'CANCELLED', name='taskstatus'), nullable=False),
        sa.Column('priority', sa.Enum('LOW', 'MEDIUM', 'HIGH', 'URGENT', name='taskpriority'), nullable=False),
        sa.Column('due_date', sa.Date(), nullable=True),
        sa.Column('project_id', sa.Integer(), nullable=False),
        sa.Column('assigned_to_id', sa.Integer(), nullable=True),
        sa.Column('created_at', sa.DateTime(), nullable=False),
        sa.Column('updated_at', sa.DateTime(), nullable=False),
        sa.ForeignKeyConstraint(['assigned_to_id'], ['users.id'], ondelete='SET NULL'),
        sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'),
        sa.PrimaryKeyConstraint('id')
    )
    op.create_index(op.f('ix_tasks_id'), 'tasks', ['id'], unique=False)
    op.create_index(op.f('ix_tasks_title'), 'tasks', ['title'], unique=False)
    op.create_index(op.f('ix_tasks_status'), 'tasks', ['status'], unique=False)
    op.create_index(op.f('ix_tasks_project_id'), 'tasks', ['project_id'], unique=False)
    op.create_index(op.f('ix_tasks_assigned_to_id'), 'tasks', ['assigned_to_id'], unique=False)
    
    # Crear tabla intermedia task_tags
    op.create_table(
        'task_tags',
        sa.Column('task_id', sa.Integer(), nullable=False),
        sa.Column('tag_id', sa.Integer(), nullable=False),
        sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ondelete='CASCADE'),
        sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], ondelete='CASCADE'),
        sa.PrimaryKeyConstraint('task_id', 'tag_id')
    )

def downgrade() -> None:
    op.drop_table('task_tags')
    op.drop_index(op.f('ix_tasks_assigned_to_id'), table_name='tasks')
    op.drop_index(op.f('ix_tasks_project_id'), table_name='tasks')
    op.drop_index(op.f('ix_tasks_status'), table_name='tasks')
    op.drop_index(op.f('ix_tasks_title'), table_name='tasks')
    op.drop_index(op.f('ix_tasks_id'), table_name='tasks')
    op.drop_table('tasks')
    op.drop_index(op.f('ix_tags_name'), table_name='tags')
    op.drop_index(op.f('ix_tags_id'), table_name='tags')
    op.drop_table('tags')
    op.drop_index(op.f('ix_projects_owner_id'), table_name='projects')
    op.drop_index(op.f('ix_projects_name'), table_name='projects')
    op.drop_index(op.f('ix_projects_id'), table_name='projects')
    op.drop_table('projects')
    op.drop_index(op.f('ix_users_username'), table_name='users')
    op.drop_index(op.f('ix_users_id'), table_name='users')
    op.drop_index(op.f('ix_users_email'), table_name='users')
    op.drop_table('users')

💡 TIPS Y OBSERVACIONES DEL INSTRUCTOR

  1. Sobre Relaciones SQLAlchemy:

    • back_populates crea relaciones bidireccionales automáticas
    • cascade="all, delete-orphan" elimina registros huérfanos automáticamente
    • lazy="dynamic" devuelve una query en lugar de cargar todos los datos
    • ondelete="CASCADE" o "SET NULL" define comportamiento a nivel de BD
  2. Sobre Foreign Keys:

    • Siempre indexar foreign keys para mejor rendimiento
    • Definir explícitamente el comportamiento ondelete
    • Usar nombres descriptivos para las columnas FK
  3. Sobre Enums:

    • SQLAlchemy Enums proporcionan validación a nivel de BD
    • Usar str, enum.Enum para compatibilidad con Pydantic
    • Los enums mejoran la integridad de datos
  4. Sobre Timestamps:

    • Usar default=datetime.utcnow (sin paréntesis) para llamada lazy
    • onupdate actualiza automáticamente el campo en cada UPDATE
    • Siempre trabajar con UTC en el backend
  5. Sobre Migraciones:

    • Revisar siempre las migraciones autogeneradas antes de aplicarlas
    • Usar mensajes descriptivos para las migraciones
    • Mantener orden cronológico claro en las versiones
    • Probar migraciones en entorno de desarrollo primero
  6. Sobre Pydantic Schemas:

    • Separar schemas de creación, actualización y respuesta
    • Usar Optional para campos que pueden no estar presentes en updates
    • from_attributes = True permite crear schemas desde modelos ORM
    • Validaciones de Pydantic complementan las de SQLAlchemy

🎬 CAPÍTULOS Y TIMESTAMPS ESTIMADOS

  • [00:00 - 03:00] Introducción y diseño del esquema
  • [03:00 - 07:00] Modelo base y timestamps
  • [07:00 - 10:00] Modelo de usuario y sus relaciones
  • [10:00 - 14:00] Modelos de proyecto y tarea
  • [14:00 - 17:00] Relación N:M con tags
  • [17:00 - 20:00] Configuración de Alembic y migraciones
  • [20:00 - 22:06] Pruebas y conclusiones

🔗 CONEXIÓN CON OTROS EPISODIOS

Depende de:

  • Video 2: Configuración inicial de SQLAlchemy
  • Video 4: Arquitectura en capas profesional

Prepara para:

  • Implementación de CRUD completo para todas las entidades
  • Endpoints de API para cada modelo
  • Implementación de WebSockets con datos reales
  • Autenticación y autorización con roles

Fundamentos establecidos:

  • Esquema completo de base de datos
  • Relaciones entre entidades
  • Sistema de migraciones
  • Validación de datos con Pydantic

📊 RESUMEN GENERAL DE LA SERIE

Duración Total

  • Video 1: 10:13 min
  • Video 2: 43:31 min
  • Video 3: 28:14 min
  • Video 4: 16:14 min
  • Video 5: 22:06 min
  • TOTAL: ~2 horas (120 minutos)

Progresión del Curso

  1. Episodio 1: Visión general y arquitectura
  2. Episodio 2: Backend inicial (el más extenso)
  3. Episodio 3: Frontend con Next.js
  4. Episodio 4: Refactorización profesional
  5. Episodio 5: Modelado de datos completo

Stack Tecnológico Completo

  • Backend: FastAPI + SQLAlchemy + Alembic + Pydantic
  • Frontend: Next.js + TypeScript + TailwindCSS + shadcn/ui
  • Base de Datos: PostgreSQL
  • Contenedorización: Podman/Docker
  • Autenticación: JWT con cookies HttpOnly
  • Comunicación en Tiempo Real: WebSockets

Conceptos Avanzados Cubiertos

✅ Arquitectura en capas ✅ Patrón Repository ✅ Inyección de dependencias ✅ Relaciones de base de datos (1:1, 1:N, N:M) ✅ Migraciones de base de datos ✅ Validación con Pydantic ✅ Contenedorización multi-servicio ✅ Type safety end-to-end


Fin del Documento 1

--------------------------------------------------


DOCUMENTO 2: DOCUMENTACIÓN TÉCNICA CONSOLIDADA

DOCUMENTO 2: DOCUMENTACIÓN TÉCNICA CONSOLIDADA

Tutorial Completo: Aplicación Full Stack con FastAPI y Next.js

De la serie "Código para Principiantes"


📋 TABLA DE CONTENIDOS

  1. Introducción al Proyecto Completo
  2. Arquitectura General de la Aplicación
  3. Configuración del Entorno
  4. Estructura Profesional del Proyecto
  5. Backend con FastAPI
  6. Frontend con Next.js
  7. Modelos y Relaciones de Datos
  8. Implementación de WebSockets
  9. Autenticación y Seguridad
  10. Mejores Prácticas Aplicadas
  11. Referencias Cruzadas entre Episodios

1. INTRODUCCIÓN AL PROYECTO COMPLETO

1.1 Visión General

Este proyecto es una aplicación Full Stack profesional que combina las tecnologías más modernas del desarrollo web:

  • Backend: FastAPI (Python) - Framework asíncrono de alto rendimiento
  • Frontend: Next.js (React + TypeScript) - Framework con SSR y optimización automática
  • Base de Datos: PostgreSQL - Sistema de gestión de bases de datos relacional robusto
  • Contenedorización: Podman/Docker - Entornos consistentes y portables

Características Principales:

  • ✅ API RESTful completa con documentación automática
  • ✅ Comunicación en tiempo real con WebSockets
  • ✅ Autenticación segura con JWT y cookies HttpOnly
  • ✅ Arquitectura escalable y profesional
  • ✅ Type safety end-to-end (Python type hints + TypeScript)
  • ✅ Sistema completo de gestión de proyectos y tareas

1.2 Objetivos del Tutorial

Al completar este tutorial, habrás construido:

  • Una API REST completa con FastAPI
  • Una aplicación frontend moderna con Next.js
  • Sistema de autenticación robusto
  • Base de datos relacional con modelos complejos
  • Comunicación en tiempo real con WebSockets
  • Aplicación completamente contenerizada

1.3 Requisitos Previos

Conocimientos necesarios:

  • Python intermedio (3.11+)
  • JavaScript/TypeScript básico
  • Conceptos de bases de datos relacionales
  • Comandos básicos de terminal
  • Fundamentos de HTTP/REST APIs

Software requerido:

  • Python 3.11 o superior
  • Node.js 20 o superior
  • PostgreSQL 15 o superior
  • Docker/Podman
  • Editor de código (VS Code recomendado)

1.4 ¿Por qué este Stack?

FastAPI:

  • Rendimiento comparable a Node.js y Go
  • Validación automática de datos con Pydantic
  • Documentación interactiva automática (Swagger/Redoc)
  • Soporte nativo para async/await
  • Type hints para código más seguro

Next.js:

  • Server-Side Rendering para mejor SEO
  • Enrutamiento automático basado en archivos
  • Optimización automática de imágenes y código
  • Excelente experiencia de desarrollo
  • Integración perfecta con React

PostgreSQL:

  • Base de datos relacional robusta y confiable
  • Soporte completo para transacciones ACID
  • Extensibilidad y rendimiento probado
  • Amplia comunidad y documentación

Podman/Docker:

  • Entornos consistentes entre desarrollo y producción
  • Fácil escalabilidad horizontal
  • Aislamiento de dependencias
  • Simplifica despliegue

2. ARQUITECTURA GENERAL DE LA APLICACIÓN

2.1 Diagrama de Arquitectura Completa

┌─────────────────────────────────────────────────────────────────┐
│                        CLIENTE (NAVEGADOR)                       │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │              NEXT.JS FRONTEND (Puerto 3000)                │ │
│  │                                                            │ │
│  │  • React Components                                       │ │
│  │  • TypeScript                                             │ │
│  │  • TailwindCSS + shadcn/ui                               │ │
│  │  • Client API (axios)                                     │ │
│  │  • WebSocket Client                                       │ │
│  └────────────────────────────────────────────────────────────┘ │
└───────────────────────────┬──────────────────────────────────────┘
                            │
                            │ HTTP/HTTPS
                            │ WebSocket (ws/wss)
                            │ JWT en Cookies
                            │
┌───────────────────────────▼──────────────────────────────────────┐
│                   FASTAPI BACKEND (Puerto 8000)                  │
│                                                                  │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                    CAPA DE API (Routers)                    ││
│  │  • Endpoints REST (/api/v1/...)                            ││
│  │  • WebSocket handlers (/ws)                                ││
│  │  • Validación de entrada (Pydantic)                        ││
│  │  • Serialización de respuestas                             ││
│  └─────────────────────────────────────────────────────────────┘│
│                            │                                     │
│  ┌─────────────────────────▼─────────────────────────────────┐  │
│  │              CAPA DE SERVICIOS (Business Logic)           │  │
│  │  • AuthService - Lógica de autenticación                 │  │
│  │  • UserService - Gestión de usuarios                     │  │
│  │  • ProjectService - Lógica de proyectos                  │  │
│  │  • WebSocketService - Gestión de conexiones              │  │
│  └─────────────────────────────────────────────────────────┘  │
│                            │                                     │
│  ┌─────────────────────────▼─────────────────────────────────┐  │
│  │           CAPA DE REPOSITORIOS (Data Access)              │  │
│  │  • CRUD genérico (BaseRepository)                        │  │
│  │  • UserRepository                                         │  │
│  │  • ProjectRepository                                      │  │
│  │  • TaskRepository                                         │  │
│  └─────────────────────────────────────────────────────────┘  │
│                            │                                     │
│  ┌─────────────────────────▼─────────────────────────────────┐  │
│  │                  CAPA DE MODELOS (ORM)                    │  │
│  │  • SQLAlchemy Models                                      │  │
│  │  • Definición de tablas y relaciones                     │  │
│  │  • User, Project, Task, Tag                              │  │
│  └─────────────────────────────────────────────────────────┘  │
└───────────────────────────┬──────────────────────────────────────┘
                            │
                            │ SQLAlchemy
                            │ Alembic (migraciones)
                            │
┌───────────────────────────▼──────────────────────────────────────┐
│                  POSTGRESQL (Puerto 5432)                         │
│                                                                  │
│  • Base de datos relacional                                      │
│  • Tablas: users, projects, tasks, tags, task_tags             │
│  • Índices, constraints, foreign keys                           │
│  • Persistencia de datos                                        │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│                    CONTENEDORES (Docker/Podman)                   │
│                                                                  │
│  [Frontend Container] ←→ [Backend Container] ←→ [DB Container]  │
│                                                                  │
│  Network: fullstack_network                                      │
│  Volumes: postgres_data                                          │
└──────────────────────────────────────────────────────────────────┘

2.2 Flujo de Datos

Flujo de Autenticación

1. Usuario → Frontend: Ingresa credenciales
2. Frontend → Backend: POST /api/v1/auth/login
3. Backend: Valida credenciales con UserService
4. Backend: Genera JWT con información del usuario
5. Backend → Frontend: Set-Cookie con JWT (HttpOnly, Secure)
6. Frontend: Almacena estado de autenticación
7. Para requests siguientes: Cookie enviada automáticamente

Flujo de Operación CRUD

1. Frontend: Usuario interactúa con UI
2. Frontend: Validación local (Zod/react-hook-form)
3. Frontend → Backend: HTTP request con JWT en cookie
4. Backend API Layer: Extrae y valida JWT
5. Backend API Layer: Valida datos de entrada (Pydantic)
6. Backend Service Layer: Ejecuta lógica de negocio
7. Backend Repository Layer: Operación en base de datos
8. PostgreSQL: Ejecuta query y retorna datos
9. Repository → Service → API: Propaga respuesta
10. Backend → Frontend: Respuesta JSON serializada
11. Frontend: Actualiza UI y estado

Flujo de WebSocket

1. Frontend: Establece conexión WebSocket
2. Backend: Acepta conexión y valida autenticación
3. Backend: Agrega cliente a ConnectionManager
4. Evento en el sistema: Cambio de estado
5. Backend: Broadcast mensaje a clientes conectados
6. Frontend: Recibe mensaje y actualiza UI en tiempo real
7. Al desconectar: Backend limpia recursos

2.3 Patrones de Arquitectura Aplicados

Clean Architecture (Arquitectura Limpia)

  • Capa Externa: API endpoints y WebSocket handlers
  • Capa de Aplicación: Servicios con lógica de negocio
  • Capa de Dominio: Modelos y entidades
  • Capa de Infraestructura: Repositorios y base de datos

Repository Pattern

  • Abstracción del acceso a datos
  • CRUD genérico reutilizable
  • Facilita testing con mocks
  • Desacopla lógica de negocio de persistencia

Service Layer Pattern

  • Contiene lógica de negocio
  • Coordina entre repositorios
  • Transacciones y validaciones complejas
  • Reutilización de lógica

Dependency Injection

  • FastAPI gestiona dependencias automáticamente
  • Facilita testing y modularidad
  • Permite composición de dependencias
  • Mejora mantenibilidad del código

3. CONFIGURACIÓN DEL ENTORNO

3.1 Instalación de Herramientas Base

Python 3.11+

# Verificar instalación
python --version  # Debe ser 3.11 o superior

# Linux (Ubuntu/Debian)
sudo apt update
sudo apt install python3.11 python3.11-venv python3-pip

# macOS (con Homebrew)
brew install python@3.11

# Windows: Descargar desde python.org

Node.js 20+

# Verificar instalación
node --version  # Debe ser 20 o superior

# Linux (usando nvm - recomendado)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 20
nvm use 20

# macOS (con Homebrew)
brew install node@20

# Windows: Descargar desde nodejs.org

PostgreSQL 15+

# Verificar instalación
psql --version

# Linux (Ubuntu/Debian)
sudo apt install postgresql postgresql-contrib

# macOS (con Homebrew)
brew install postgresql@15

# Windows: Descargar desde postgresql.org

# Iniciar servicio
sudo systemctl start postgresql  # Linux
brew services start postgresql   # macOS

Docker/Podman

# Docker (Linux)
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# Podman (alternativa sin daemon)
# Linux (Ubuntu/Debian)
sudo apt install podman podman-compose

# macOS
brew install podman

3.2 Estructura de Directorios del Proyecto

fullstack-fastapi-nextjs/
│
├── backend/                      # Aplicación FastAPI
│   ├── app/
│   │   ├── __init__.py
│   │   ├── main.py
│   │   ├── api/
│   │   │   ├── __init__.py
│   │   │   ├── deps.py
│   │   │   └── v1/
│   │   │       ├── __init__.py
│   │   │       ├── router.py
│   │   │       └── endpoints/
│   │   │           ├── __init__.py
│   │   │           ├── auth.py
│   │   │           ├── users.py
│   │   │           ├── projects.py
│   │   │           ├── tasks.py
│   │   │           └── websocket.py
│   │   ├── core/
│   │   │   ├── __init__.py
│   │   │   ├── config.py
│   │   │   ├── security.py
│   │   │   └── exceptions.py
│   │   ├── db/
│   │   │   ├── __init__.py
│   │   │   ├── base.py
│   │   │   ├── session.py
│   │   │   └── init_db.py
│   │   ├── models/
│   │   │   ├── __init__.py
│   │   │   ├── base.py
│   │   │   ├── user.py
│   │   │   ├── project.py
│   │   │   ├── task.py
│   │   │   └── tag.py
│   │   ├── schemas/
│   │   │   ├── __init__.py
│   │   │   ├── user.py
│   │   │   ├── project.py
│   │   │   ├── task.py
│   │   │   ├── tag.py
│   │   │   └── token.py
│   │   ├── services/
│   │   │   ├── __init__.py
│   │   │   ├── auth_service.py
│   │   │   ├── user_service.py
│   │   │   ├── project_service.py
│   │   │   └── websocket_service.py
│   │   ├── repositories/
│   │   │   ├── __init__.py
│   │   │   ├── base.py
│   │   │   ├── user_repo.py
│   │   │   ├── project_repo.py
│   │   │   └── task_repo.py
│   │   └── utils/
│   │       ├── __init__.py
│   │       └── helpers.py
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── conftest.py
│   │   ├── api/
│   │   ├── services/
│   │   └── repositories/
│   ├── alembic/
│   │   ├── versions/
│   │   └── env.py
│   ├── .env
│   ├── .env.example
│   ├── requirements.txt
│   ├── alembic.ini
│   └── Dockerfile
│
├── frontend/                     # Aplicación Next.js
│   ├── src/
│   │   ├── app/
│   │   │   ├── layout.tsx
│   │   │   ├── page.tsx
│   │   │   ├── (auth)/
│   │   │   │   ├── login/
│   │   │   │   │   └── page.tsx
│   │   │   │   └── register/
│   │   │   │       └── page.tsx
│   │   │   └── (protected)/
│   │   │       ├── dashboard/
│   │   │       │   └── page.tsx
│   │   │       ├── projects/
│   │   │       │   ├── page.tsx
│   │   │       │   └── [id]/
│   │   │       │       └── page.tsx
│   │   │       └── tasks/
│   │   │           └── page.tsx
│   │   ├── components/
│   │   │   ├── ui/              # shadcn/ui components
│   │   │   │   ├── button.tsx
│   │   │   │   ├── input.tsx
│   │   │   │   ├── card.tsx
│   │   │   │   ├── dialog.tsx
│   │   │   │   └── ...
│   │   │   ├── layout/
│   │   │   │   ├── Header.tsx
│   │   │   │   ├── Footer.tsx
│   │   │   │   ├── Sidebar.tsx
│   │   │   │   └── Navigation.tsx
│   │   │   └── forms/
│   │   │       ├── LoginForm.tsx
│   │   │       ├── RegisterForm.tsx
│   │   │       ├── ProjectForm.tsx
│   │   │       └── TaskForm.tsx
│   │   ├── lib/
│   │   │   ├── api.ts           # Cliente API
│   │   │   ├── auth.ts          # Lógica de autenticación
│   │   │   └── utils.ts         # Utilidades
│   │   ├── hooks/
│   │   │   ├── useAuth.ts
│   │   │   ├── useWebSocket.ts
│   │   │   └── useApi.ts
│   │   ├── types/
│   │   │   └── api.ts           # Tipos TypeScript
│   │   └── styles/
│   │       └── globals.css
│   ├── public/
│   │   └── assets/
│   ├── .env.local
│   ├── .env.example
│   ├── next.config.js
│   ├── tailwind.config.ts
│   ├── tsconfig.json
│   ├── package.json
│   └── Dockerfile
│
├── docker-compose.yml            # Orquestación de contenedores
├── .gitignore
└── README.md

3.3 Configuración del Backend (FastAPI)

Crear entorno virtual y instalar dependencias

# Navegar al directorio backend
cd fullstack-fastapi-nextjs/backend

# Crear entorno virtual
python -m venv venv

# Activar entorno virtual
# Linux/macOS:
source venv/bin/activate
# Windows:
venv\Scripts\activate

# Instalar dependencias
pip install -r requirements.txt

requirements.txt

# FastAPI y servidor ASGI
fastapi==0.109.0
uvicorn[standard]==0.27.0

# Base de datos y ORM
sqlalchemy==2.0.25
psycopg2-binary==2.9.9
alembic==1.13.1

# Validación y serialización
pydantic==2.5.3
pydantic-settings==2.1.0
pydantic[email]==2.5.3

# Autenticación y seguridad
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6

# CORS y middleware
python-dotenv==1.0.0

# WebSockets
websockets==12.0

# Testing
pytest==7.4.4
pytest-asyncio==0.23.3
httpx==0.26.0

# Utilidades
python-dateutil==2.8.2

Configuración de variables de entorno (.env)

# Application
APP_NAME="FastAPI Full Stack"
VERSION="1.0.0"
DEBUG=True
ENVIRONMENT="development"

# Database
DATABASE_URL=postgresql://fastapi_user:SecurePassword123!@localhost:5432/fullstack_db

# Security
SECRET_KEY=09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7

# CORS
ALLOWED_ORIGINS=["http://localhost:3000","http://frontend:3000"]

# Server
HOST=0.0.0.0
PORT=8000
RELOAD=True

Generar SECRET_KEY seguro

# Python
python -c "import secrets; print(secrets.token_urlsafe(32))"

# OpenSSL
openssl rand -hex 32

Configurar PostgreSQL

# Conectar a PostgreSQL
sudo -u postgres psql

# Crear usuario y base de datos
CREATE USER fastapi_user WITH PASSWORD 'SecurePassword123!';
CREATE DATABASE fullstack_db OWNER fastapi_user;
GRANT ALL PRIVILEGES ON DATABASE fullstack_db TO fastapi_user;

# Salir
\q

3.4 Configuración del Frontend (Next.js)

Crear proyecto e instalar dependencias

# Navegar al directorio del proyecto
cd fullstack-fastapi-nextjs

# Crear aplicación Next.js
npx create-next-app@latest frontend --typescript --tailwind --app --src-dir

# Navegar a frontend
cd frontend

# Instalar dependencias adicionales
npm install axios react-hook-form zod @hookform/resolvers
npm install openapi-fetch openapi-typescript

# Instalar shadcn/ui
npx shadcn-ui@latest init

# Instalar componentes específicos
npx shadcn-ui@latest add button
npx shadcn-ui@latest add input
npx shadcn-ui@latest add card
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add form
npx shadcn-ui@latest add label
npx shadcn-ui@latest add select
npx shadcn-ui@latest add toast

Variables de entorno (.env.local)

# API Backend
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
NEXT_PUBLIC_WS_URL=ws://localhost:8000/ws

# Application
NEXT_PUBLIC_APP_NAME="FastAPI Full Stack"
NEXT_PUBLIC_APP_VERSION="1.0.0"

3.5 Configuración de Docker/Podman

docker-compose.yml completo

version: '3.8'

services:
  # PostgreSQL Database
  postgres:
    image: postgres:15-alpine
    container_name: fullstack_postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: fastapi_user
      POSTGRES_PASSWORD: SecurePassword123!
      POSTGRES_DB: fullstack_db
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    networks:
      - fullstack_network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U fastapi_user -d fullstack_db"]
      interval: 10s
      timeout: 5s
      retries: 5

  # FastAPI Backend
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: fullstack_backend
    restart: unless-stopped
    env_file:
      - ./backend/.env
    environment:
      - DATABASE_URL=postgresql://fastapi_user:SecurePassword123!@postgres:5432/fullstack_db
    volumes:
      - ./backend/app:/app/app
      - ./backend/alembic:/app/alembic
    ports:
      - "8000:8000"
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - fullstack_network
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

  # Next.js Frontend
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
      args:
        NEXT_PUBLIC_API_URL: http://backend:8000/api/v1
        NEXT_PUBLIC_WS_URL: ws://backend:8000/ws
    container_name: fullstack_frontend
    restart: unless-stopped
    environment:
      - NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
      - NEXT_PUBLIC_WS_URL=ws://localhost:8000/ws
    volumes:
      - ./frontend/src:/app/src
      - ./frontend/public:/app/public
      - /app/node_modules
      - /app/.next
    ports:
      - "3000:3000"
    depends_on:
      - backend
    networks:
      - fullstack_network

networks:
  fullstack_network:
    driver: bridge

volumes:
  postgres_data:
    driver: local

Comandos Docker/Podman esenciales

# Construir imágenes
docker-compose build

# Levantar servicios
docker-compose up -d

# Ver logs
docker-compose logs -f
docker-compose logs -f backend
docker-compose logs -f frontend

# Detener servicios
docker-compose stop

# Detener y eliminar contenedores
docker-compose down

# Eliminar incluyendo volúmenes
docker-compose down -v

# Reconstruir sin caché
docker-compose build --no-cache

# Ejecutar comandos en contenedor
docker-compose exec backend bash
docker-compose exec backend alembic upgrade head

# Ver estado de contenedores
docker-compose ps

# Reiniciar servicio específico
docker-compose restart backend

3.6 Verificación de la Instalación

Verificar Backend

# Activar entorno virtual
source backend/venv/bin/activate

# Ejecutar servidor
cd backend
uvicorn app.main:app --reload

# Verificar en navegador:
# http://localhost:8000/health
# http://localhost:8000/docs (Swagger UI)
# http://localhost:8000/redoc (ReDoc)

Verificar Frontend

# Ejecutar servidor de desarrollo
cd frontend
npm run dev

# Verificar en navegador:
# http://localhost:3000

Verificar PostgreSQL

# Conectar a la base de datos
psql -U fastapi_user -d fullstack_db -h localhost

# Verificar tablas (después de migraciones)
\dt

# Salir
\q

4. ESTRUCTURA PROFESIONAL DEL PROYECTO

4.1 Organización del Backend

Principios de Organización

  1. Separación de Responsabilidades: Cada módulo tiene una función específica
  2. Modularidad: Código organizado en paquetes reutilizables
  3. Escalabilidad: Fácil agregar nuevas características
  4. Testabilidad: Estructura facilita pruebas unitarias
  5. Mantenibilidad: Código claro y bien documentado

Descripción de Módulos

app/main.py - Punto de entrada

  • Crea instancia de FastAPI
  • Configura middleware (CORS, etc.)
  • Incluye routers
  • Define eventos de startup/shutdown
  • Configura documentación

app/core/ - Configuración central

  • config.py: Settings con Pydantic
  • security.py: JWT, hashing, autenticación
  • exceptions.py: Excepciones personalizadas

app/api/ - Capa de presentación

  • deps.py: Dependencias compartidas
  • v1/router.py: Router principal versionado
  • v1/endpoints/: Endpoints específicos

app/services/ - Lógica de negocio

  • Coordina operaciones complejas
  • Valida reglas de negocio
  • Orquesta entre repositorios
  • Maneja transacciones

app/repositories/ - Acceso a datos

  • CRUD genérico (base.py)
  • Operaciones específicas por entidad
  • Abstrae SQLAlchemy
  • Facilita testing con mocks

app/models/ - Modelos ORM

  • Definición de tablas con SQLAlchemy
  • Relaciones entre entidades
  • Constraints e índices

app/schemas/ - Esquemas Pydantic

  • Validación de entrada/salida
  • Serialización automática
  • Documentación de API

app/db/ - Configuración de BD

  • Conexión a PostgreSQL
  • Session management
  • Inicialización de BD

4.2 Organización del Frontend

Principios de Organización

  1. Component-Based: Todo es un componente reutilizable
  2. Type Safety: TypeScript en todo el código
  3. Separation of Concerns: Lógica separada de presentación
  4. Performance: Server Components por defecto
  5. Accessibility: Componentes accesibles (a11y)

Descripción de Módulos

src/app/ - Rutas de la aplicación

  • Next.js App Router
  • Grupos de rutas: (auth), (protected)
  • Layouts compartidos
  • Páginas específicas

src/components/ - Componentes React

  • ui/: Componentes de shadcn/ui
  • layout/: Header, Footer, Sidebar
  • forms/: Formularios específicos

src/lib/ - Librerías y utilidades

  • api.ts: Cliente HTTP (axios)
  • auth.ts: Lógica de autenticación
  • utils.ts: Funciones auxiliares

src/hooks/ - React Hooks personalizados

  • useAuth: Gestión de autenticación
  • useWebSocket: Conexión WebSocket
  • useApi: Llamadas a API

src/types/ - Definiciones TypeScript

  • Interfaces compartidas
  • Tipos del API

4.3 Convenciones de Nomenclatura

Backend (Python)

# Archivos y módulos: snake_case
user_service.py
auth_service.py

# Clases: PascalCase
class UserService:
class ProjectRepository:

# Funciones y variables: snake_case
def get_current_user():
user_id = 123

# Constantes: UPPER_SNAKE_CASE
SECRET_KEY = "..."
MAX_UPLOAD_SIZE = 10485760

# Modelos SQLAlchemy: PascalCase
class User(Base):
class Project(Base):

# Esquemas Pydantic: PascalCase
class UserCreate(BaseModel):
class UserResponse(BaseModel):

Frontend (TypeScript)

// Archivos de componentes: PascalCase
UserProfile.tsx
LoginForm.tsx

// Archivos de utilidades: camelCase
api.ts
auth.ts

// Componentes: PascalCase
function UserProfile() {}
const LoginForm: React.FC = () => {}

// Funciones y variables: camelCase
const getUserData = () => {}
const userId = 123

// Constantes: UPPER_SNAKE_CASE
const API_URL = "..."
const MAX_RETRIES = 3

// Tipos e Interfaces: PascalCase
interface User {}
type ApiResponse<T> = {}

// Enums: PascalCase
enum TaskStatus {}

5. BACKEND CON FASTAPI

5.1 Aplicación Principal (main.py)

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import settings
from app.api.v1.router import api_router
from app.db.session import engine
from app.db.base import Base

# Crear instancia de FastAPI
app = FastAPI(
    title=settings.APP_NAME,
    version=settings.VERSION,
    description="API Full Stack Profesional con FastAPI y Next.js",
    openapi_url=f"/api/v1/openapi.json",
    docs_url="/api/docs",
    redoc_url="/api/redoc",
    swagger_ui_parameters={"persistAuthorization": True}
)

# Configurar CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.ALLOWED_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
    expose_headers=["*"]
)

# Incluir routers
app.include_router(api_router, prefix="/api/v1")

# Health check endpoint
@app.get("/health", tags=["health"])
async def health_check():
    """Endpoint de health check"""
    return {
        "status": "healthy",
        "version": settings.VERSION,
        "environment": settings.ENVIRONMENT
    }

# Eventos de ciclo de vida
@app.on_event("startup")
async def startup_event():
    """Ejecutar al iniciar la aplicación"""
    print(f"🚀 {settings.APP_NAME} v{settings.VERSION} iniciado")
    print(f"📚 Documentación: http://{settings.HOST}:{settings.PORT}/api/docs")
    print(f"🔧 Entorno: {settings.ENVIRONMENT}")

@app.on_event("shutdown")
async def shutdown_event():
    """Ejecutar al cerrar la aplicación"""
    print("👋 Cerrando aplicación...")

5.2 Configuración (core/config.py)

from typing import List
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import AnyHttpUrl, field_validator

class Settings(BaseSettings):
    """Configuración de la aplicación"""
    
    # Application
    APP_NAME: str
    VERSION: str = "1.0.0"
    DEBUG: bool = False
    ENVIRONMENT: str = "development"
    
    # Server
    HOST: str = "0.0.0.0"
    PORT: int = 8000
    RELOAD: bool = False
    
    # Database
    DATABASE_URL: str
    
    # Security
    SECRET_KEY: str
    ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
    REFRESH_TOKEN_EXPIRE_DAYS: int = 7
    
    # CORS
    ALLOWED_ORIGINS: List[AnyHttpUrl] = []
    
    @field_validator("ALLOWED_ORIGINS", mode="before")
    @classmethod
    def parse_origins(cls, v):
        if isinstance(v, str):
            return [i.strip() for i in v.split(",")]
        return v
    
    model_config = SettingsConfigDict(
        env_file=".env",
        case_sensitive=True,
        extra="ignore"
    )

# Instancia global de settings
settings = Settings()

5.3 Seguridad (core/security.py)

from datetime import datetime, timedelta
from typing import Optional
from jose import jwt, JWTError
from passlib.context import CryptContext
from app.core.config import settings

# Configurar bcrypt para hashing de contraseñas
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """Verificar contraseña con hash"""
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    """Generar hash de contraseña"""
    return pwd_context.hash(password)

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """Crear JWT access token"""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
    
    to_encode.update({"exp": expire, "iat": datetime.utcnow()})
    encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
    return encoded_jwt

def create_refresh_token(data: dict) -> str:
    """Crear JWT refresh token"""
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
    to_encode.update({"exp": expire, "iat": datetime.utcnow(), "type": "refresh"})
    encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
    return encoded_jwt

def decode_token(token: str) -> Optional[dict]:
    """Decodificar y validar JWT"""
    try:
        payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
        return payload
    except JWTError:
        return None

5.4 Dependencias (api/deps.py)

from typing import Generator, Optional
from fastapi import Depends, HTTPException, status, Cookie
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.security import decode_token
from app.db.session import SessionLocal
from app.models.user import User
from app.services.user_service import user_service

# OAuth2 scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login", auto_error=False)

def get_db() -> Generator:
    """
    Dependency para obtener sesión de base de datos
    """
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

async def get_current_user(
    db: Session = Depends(get_db),
    token: Optional[str] = Depends(oauth2_scheme),
    access_token: Optional[str] = Cookie(None)
) -> User:
    """
    Dependency para obtener usuario actual desde JWT
    Prioriza cookie sobre header Authorization
    """
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="No se pudo validar las credenciales",
        headers={"WWW-Authenticate": "Bearer"},
    )
    
    # Priorizar token de cookie
    token_to_decode = access_token or token
    
    if not token_to_decode:
        raise credentials_exception
    
    payload = decode_token(token_to_decode)
    if payload is None:
        raise credentials_exception
    
    user_id: str = payload.get("sub")
    if user_id is None:
        raise credentials_exception
    
    user = user_service.get_user(db, user_id=int(user_id))
    if user is None:
        raise credentials_exception
    
    return user

async def get_current_active_user(
    current_user: User = Depends(get_current_user),
) -> User:
    """
    Dependency para verificar que usuario esté activo
    """
    if not current_user.is_active:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST, 
            detail="Usuario inactivo"
        )
    return current_user

async def get_current_active_superuser(
    current_user: User = Depends(get_current_user),
) -> User:
    """
    Dependency para verificar que usuario sea superusuario
    """
    if not current_user.is_superuser:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Permisos insuficientes"
        )
    return current_user

5.5 Repositorio Base (repositories/base.py)

from typing import TypeVar, Generic, Type, Optional, List, Any
from sqlalchemy.orm import Session
from sqlalchemy import select, update, delete
from app.db.base import Base

ModelType = TypeVar("ModelType", bound=Base)

class BaseRepository(Generic[ModelType]):
    """
    Repositorio base con operaciones CRUD genéricas
    """
    def __init__(self, model: Type[ModelType]):
        self.model = model
    
    def get(self, db: Session, id: int) -> Optional[ModelType]:
        """Obtener por ID"""
        return db.query(self.model).filter(self.model.id == id).first()
    
    def get_multi(
        self, 
        db: Session, 
        *, 
        skip: int = 0, 
        limit: int = 100
    ) -> List[ModelType]:
        """Obtener múltiples registros"""
        return db.query(self.model).offset(skip).limit(limit).all()
    
    def get_by_field(
        self,
        db: Session,
        *,
        field_name: str,
        value: Any
    ) -> Optional[ModelType]:
        """Obtener por campo específico"""
        return db.query(self.model).filter(
            getattr(self.model, field_name) == value
        ).first()
    
    def create(self, db: Session, *, obj_in: dict) -> ModelType:
        """Crear nuevo registro"""
        db_obj = self.model(**obj_in)
        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj
    
    def update(
        self, 
        db: Session, 
        *, 
        db_obj: ModelType, 
        obj_in: dict
    ) -> ModelType:
        """Actualizar registro existente"""
        for field, value in obj_in.items():
            setattr(db_obj, field, value)
        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj
    
    def delete(self, db: Session, *, id: int) -> Optional[ModelType]:
        """Eliminar registro"""
        obj = db.query(self.model).get(id)
        if obj:
            db.delete(obj)
            db.commit()
        return obj
    
    def count(self, db: Session) -> int:
        """Contar registros"""
        return db.query(self.model).count()

5.6 Endpoints de Autenticación (api/v1/endpoints/auth.py)

from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status, Response
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.api import deps
from app.core.config import settings
from app.core.security import create_access_token, create_refresh_token
from app.schemas.user import UserCreate, User as UserSchema
from app.schemas.token import Token
from app.services.auth_service import auth_service

router = APIRouter()

@router.post("/register", response_model=UserSchema, status_code=status.HTTP_201_CREATED)
def register(
    *,
    db: Session = Depends(deps.get_db),
    user_in: UserCreate
):
    """
    Registrar nuevo usuario
    """
    user = auth_service.register_user(db, user_in=user_in)
    return user

@router.post("/login", response_model=Token)
def login(
    *,
    db: Session = Depends(deps.get_db),
    form_data: OAuth2PasswordRequestForm = Depends(),
    response: Response
):
    """
    Login de usuario con OAuth2 password flow
    """
    user = auth_service.authenticate(
        db, 
        email=form_data.username,  # OAuth2 usa "username"
        password=form_data.password
    )
    
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Email o contraseña incorrectos",
            headers={"WWW-Authenticate": "Bearer"},
        )
    
    # Crear tokens
    access_token = create_access_token(data={"sub": str(user.id)})
    refresh_token = create_refresh_token(data={"sub": str(user.id)})
    
    # Configurar cookies
    response.set_cookie(
        key="access_token",
        value=access_token,
        httponly=True,
        secure=not settings.DEBUG,  # HTTPS en producción
        samesite="strict",
        max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
    )
    
    response.set_cookie(
        key="refresh_token",
        value=refresh_token,
        httponly=True,
        secure=not settings.DEBUG,
        samesite="strict",
        max_age=settings.REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
    )
    
    return {
        "access_token": access_token,
        "refresh_token": refresh_token,
        "token_type": "bearer"
    }

@router.post("/logout")
def logout(response: Response):
    """
    Logout - eliminar cookies
    """
    response.delete_cookie(key="access_token")
    response.delete_cookie(key="refresh_token")
    return {"message": "Logout exitoso"}

@router.get("/me", response_model=UserSchema)
def read_users_me(
    current_user: User = Depends(deps.get_current_active_user),
):
    """
    Obtener usuario actual
    """
    return current_user

(Continúa en la siguiente parte debido al límite de caracteres...)


6. FRONTEND CON NEXT.JS

6.1 Cliente API (lib/api.ts)

import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';

// Configuración base de axios
const apiClient: AxiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1',
  timeout: 10000,
  withCredentials: true, // Enviar cookies automáticamente
  headers: {
    'Content-Type': 'application/json',
  },
});

// Interceptor para requests
apiClient.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    // Logs en desarrollo
    if (process.env.NODE_ENV === 'development') {
      console.log(`📡 ${config.method?.toUpperCase()} ${config.url}`);
    }
    return config;
  },
  (error: AxiosError) => {
    return Promise.reject(error);
  }
);

// Interceptor para responses
apiClient.interceptors.response.use(
  (response) => {
    // Logs en desarrollo
    if (process.env.NODE_ENV === 'development') {
      console.log(`✅ ${response.status} ${response.config.url}`);
    }
    return response;
  },
  async (error: AxiosError) => {
    const originalRequest = error.config;
    
    // Redirigir a login si 401
    if (error.response?.status === 401 && typeof window !== 'undefined') {
      window.location.href = '/login';
    }
    
    // Logs de error
    if (process.env.NODE_ENV === 'development') {
      console.error(`❌ ${error.response?.status} ${error.config?.url}`, error.response?.data);
    }
    
    return Promise.reject(error);
  }
);

// API functions
export const api = {
  // Auth
  auth: {
    login: async (email: string, password: string) => {
      const formData = new FormData();
      formData.append('username', email); // OAuth2 usa "username"
      formData.append('password', password);
      
      const response = await apiClient.post('/auth/login', formData, {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
      });
      return response.data;
    },
    
    register: async (userData: any) => {
      const response = await apiClient.post('/auth/register', userData);
      return response.data;
    },
    
    logout: async () => {
      const response = await apiClient.post('/auth/logout');
      return response.data;
    },
    
    getCurrentUser: async () => {
      const response = await apiClient.get('/auth/me');
      return response.data;
    },
  },
  
  // Users
  users: {
    list: async (skip: number = 0, limit: number = 100) => {
      const response = await apiClient.get('/users/', { params: { skip, limit } });
      return response.data;
    },
    
    get: async (userId: number) => {
      const response = await apiClient.get(`/users/${userId}`);
      return response.data;
    },
    
    update: async (userId: number, userData: any) => {
      const response = await apiClient.put(`/users/${userId}`, userData);
      return response.data;
    },
  },
  
  // Projects
  projects: {
    list: async (skip: number = 0, limit: number = 100) => {
      const response = await apiClient.get('/projects/', { params: { skip, limit } });
      return response.data;
    },
    
    get: async (projectId: number) => {
      const response = await apiClient.get(`/projects/${projectId}`);
      return response.data;
    },
    
    create: async (projectData: any) => {
      const response = await apiClient.post('/projects/', projectData);
      return response.data;
    },
    
    update: async (projectId: number, projectData: any) => {
      const response = await apiClient.put(`/projects/${projectId}`, projectData);
      return response.data;
    },
    
    delete: async (projectId: number) => {
      const response = await apiClient.delete(`/projects/${projectId}`);
      return response.data;
    },
  },
  
  // Tasks
  tasks: {
    list: async (projectId?: number, skip: number = 0, limit: number = 100) => {
      const response = await apiClient.get('/tasks/', { 
        params: { project_id: projectId, skip, limit } 
      });
      return response.data;
    },
    
    get: async (taskId: number) => {
      const response = await apiClient.get(`/tasks/${taskId}`);
      return response.data;
    },
    
    create: async (taskData: any) => {
      const response = await apiClient.post('/tasks/', taskData);
      return response.data;
    },
    
    update: async (taskId: number, taskData: any) => {
      const response = await apiClient.put(`/tasks/${taskId}`, taskData);
      return response.data;
    },
    
    delete: async (taskId: number) => {
      const response = await apiClient.delete(`/tasks/${taskId}`);
      return response.data;
    },
  },
};

export default apiClient;

6.2 Hook de Autenticación (hooks/useAuth.ts)

import { useState, useEffect, useCallback } from 'react';
import { api } from '@/lib/api';

interface User {
  id: number;
  username: string;
  email: string;
  full_name: string | null;
  is_active: boolean;
  is_superuser: boolean;
}

interface AuthState {
  user: User | null;
  isLoading: boolean;
  isAuthenticated: boolean;
}

export function useAuth() {
  const [authState, setAuthState] = useState<AuthState>({
    user: null,
    isLoading: true,
    isAuthenticated: false,
  });

  // Cargar usuario actual al montar
  useEffect(() => {
    loadUser();
  }, []);

  const loadUser = async () => {
    try {
      const user = await api.auth.getCurrentUser();
      setAuthState({
        user,
        isLoading: false,
        isAuthenticated: true,
      });
    } catch (error) {
      setAuthState({
        user: null,
        isLoading: false,
        isAuthenticated: false,
      });
    }
  };

  const login = useCallback(async (email: string, password: string) => {
    try {
      await api.auth.login(email, password);
      await loadUser();
      return { success: true };
    } catch (error: any) {
      return {
        success: false,
        error: error.response?.data?.detail || 'Error al iniciar sesión',
      };
    }
  }, []);

  const register = useCallback(async (userData: any) => {
    try {
      await api.auth.register(userData);
      return { success: true };
    } catch (error: any) {
      return {
        success: false,
        error: error.response?.data?.detail || 'Error al registrarse',
      };
    }
  }, []);

  const logout = useCallback(async () => {
    try {
      await api.auth.logout();
      setAuthState({
        user: null,
        isLoading: false,
        isAuthenticated: false,
      });
      // Redirigir a login
      if (typeof window !== 'undefined') {
        window.location.href = '/login';
      }
    } catch (error) {
      console.error('Error al cerrar sesión:', error);
    }
  }, []);

  return {
    user: authState.user,
    isLoading: authState.isLoading,
    isAuthenticated: authState.isAuthenticated,
    login,
    register,
    logout,
    reload: loadUser,
  };
}

7. MODELOS Y RELACIONES DE DATOS

7.1 Diagrama Entidad-Relación

┌────────────────────────────────────────────────────────────────────┐
│                      ESQUEMA DE BASE DE DATOS                      │
└────────────────────────────────────────────────────────────────────┘

┌──────────────────┐
│      USER        │
├──────────────────┤
│ • id (PK)        │
│ • username (UQ)  │
│ • email (UQ)     │
│ • full_name      │
│ • hashed_password│
│ • is_active      │
│ • is_superuser   │
│ • is_verified    │
│ • created_at     │
│ • updated_at     │
└──────────────────┘
        │ 1
        │ owns
        │ N
        ▼
┌──────────────────┐
│     PROJECT      │
├──────────────────┤
│ • id (PK)        │
│ • name           │
│ • description    │
│ • is_active      │
│ • owner_id (FK)  │◄─────┐
│ • created_at     │      │ references
│ • updated_at     │      │
└──────────────────┘      │
        │ 1               │
        │ contains        │
        │ N               │
        ▼                 │
┌──────────────────┐      │
│      TASK        │      │
├──────────────────┤      │
│ • id (PK)        │      │
│ • title          │      │
│ • description    │      │
│ • status         │      │
│ • priority       │      │
│ • due_date       │      │
│ • project_id (FK)│──────┘
│ • assigned_to_id │─────┐
│ • created_at     │     │ references USER
│ • updated_at     │     │ (optional)
└──────────────────┘     │
        │ N               │
        │ tagged with     │
        │ M               ▼
        ▼              ┌──────────────────┐
┌──────────────────┐  │    (implicit)    │
│    TASK_TAG      │  └──────────────────┘
│  (junction table)│
├──────────────────┤
│ • task_id (FK,PK)│
│ • tag_id (FK,PK) │
└──────────────────┘
        │ M
        │ references
        │ 1
        ▼
┌──────────────────┐
│       TAG        │
├──────────────────┤
│ • id (PK)        │
│ • name (UQ)      │
│ • color          │
│ • created_at     │
│ • updated_at     │
└──────────────────┘

LEYENDA:
PK  = Primary Key
FK  = Foreign Key
UQ  = Unique Constraint
1   = Relación uno
N   = Relación muchos
M   = Relación muchos (en N:M)

7.2 Tipos de Relaciones Implementadas

Relación 1:N (Uno a Muchos)

User → Projects

  • Un usuario puede tener múltiples proyectos
  • Cada proyecto pertenece a un único usuario (owner)
  • Cascade: Al eliminar usuario, se eliminan sus proyectos

Project → Tasks

  • Un proyecto contiene múltiples tareas
  • Cada tarea pertenece a un único proyecto
  • Cascade: Al eliminar proyecto, se eliminan sus tareas

Relación N:1 (Muchos a Uno - Opcional)

Task → User (assigned_to)

  • Una tarea puede estar asignada a un usuario (o ninguno)
  • Un usuario puede tener múltiples tareas asignadas
  • ON DELETE SET NULL: Al eliminar usuario, las tareas quedan sin asignar

Relación N:M (Muchos a Muchos)

Task ↔ Tag

  • Una tarea puede tener múltiples tags
  • Un tag puede estar en múltiples tareas
  • Tabla intermedia: task_tags
  • Cascade en ambas direcciones

7.3 Código de Modelos Completo

Ya documentado en la sección del DOCUMENTO 1 (Video 5).


8. IMPLEMENTACIÓN DE WEBSOCKETS

8.1 Conceptos de WebSockets

¿Qué son los WebSockets?

  • Protocolo de comunicación bidireccional full-duplex
  • Conexión persistente entre cliente y servidor
  • Ideal para aplicaciones en tiempo real
  • Baja latencia comparado con polling HTTP

Casos de uso en este proyecto:

  • Notificaciones de cambios en proyectos/tareas
  • Actualizaciones en tiempo real del dashboard
  • Chat o mensajería entre usuarios
  • Sincronización de estado entre clientes

8.2 Backend: WebSocket Manager

services/websocket_service.py:

from typing import Set, Dict
from fastapi import WebSocket, WebSocketDisconnect
import json
from datetime import datetime

class ConnectionManager:
    """
    Gestor de conexiones WebSocket
    """
    def __init__(self):
        # Almacenar conexiones activas por user_id
        self.active_connections: Dict[int, Set[WebSocket]] = {}
    
    async def connect(self, websocket: WebSocket, user_id: int):
        """Conectar cliente WebSocket"""
        await websocket.accept()
        
        if user_id not in self.active_connections:
            self.active_connections[user_id] = set()
        
        self.active_connections[user_id].add(websocket)
        print(f"✅ Usuario {user_id} conectado. Total conexiones: {self.get_connection_count()}")
    
    def disconnect(self, websocket: WebSocket, user_id: int):
        """Desconectar cliente WebSocket"""
        if user_id in self.active_connections:
            self.active_connections[user_id].discard(websocket)
            
            # Limpiar si no hay más conexiones
            if not self.active_connections[user_id]:
                del self.active_connections[user_id]
        
        print(f"❌ Usuario {user_id} desconectado. Total conexiones: {self.get_connection_count()}")
    
    async def send_personal_message(self, message: dict, user_id: int):
        """Enviar mensaje a un usuario específico"""
        if user_id in self.active_connections:
            message_json = json.dumps(message)
            for connection in list(self.active_connections[user_id]):
                try:
                    await connection.send_text(message_json)
                except Exception as e:
                    print(f"Error enviando mensaje a usuario {user_id}: {e}")
                    self.disconnect(connection, user_id)
    
    async def broadcast(self, message: dict, exclude_user: int = None):
        """
        Broadcast mensaje a todos los usuarios conectados
        Opcionalmente excluir un usuario específico
        """
        message_json = json.dumps(message)
        
        for user_id, connections in list(self.active_connections.items()):
            if exclude_user and user_id == exclude_user:
                continue
            
            for connection in list(connections):
                try:
                    await connection.send_text(message_json)
                except Exception as e:
                    print(f"Error en broadcast a usuario {user_id}: {e}")
                    self.disconnect(connection, user_id)
    
    async def broadcast_to_project_members(self, message: dict, project_id: int, db):
        """
        Broadcast a usuarios de un proyecto específico
        """
        # Aquí implementarías lógica para obtener miembros del proyecto
        # y enviar solo a ellos
        pass
    
    def get_connection_count(self) -> int:
        """Obtener número total de conexiones"""
        return sum(len(connections) for connections in self.active_connections.values())
    
    def get_connected_users(self) -> list:
        """Obtener lista de user_ids conectados"""
        return list(self.active_connections.keys())

# Instancia global del manager
websocket_manager = ConnectionManager()

Endpoint WebSocket (api/v1/endpoints/websocket.py):

from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends
from sqlalchemy.orm import Session
from app.api import deps
from app.services.websocket_service import websocket_manager
from app.core.security import decode_token
import json

router = APIRouter()

@router.websocket("/ws/{token}")
async def websocket_endpoint(
    websocket: WebSocket,
    token: str,
    db: Session = Depends(deps.get_db)
):
    """
    Endpoint WebSocket para comunicación en tiempo real
    
    Cliente debe proveer token JWT en la URL
    """
    # Validar token
    payload = decode_token(token)
    if not payload:
        await websocket.close(code=1008)  # Policy violation
        return
    
    user_id = int(payload.get("sub"))
    
    # Conectar
    await websocket_manager.connect(websocket, user_id)
    
    try:
        # Enviar mensaje de bienvenida
        await websocket.send_json({
            "type": "connection",
            "message": "Conectado exitosamente",
            "user_id": user_id,
            "timestamp": datetime.utcnow().isoformat()
        })
        
        # Loop para recibir mensajes
        while True:
            # Recibir mensaje del cliente
            data = await websocket.receive_text()
            message_data = json.loads(data)
            
            # Procesar según tipo de mensaje
            message_type = message_data.get("type")
            
            if message_type == "ping":
                # Responder pong
                await websocket.send_json({
                    "type": "pong",
                    "timestamp": datetime.utcnow().isoformat()
                })
            
            elif message_type == "broadcast":
                # Broadcast a todos
                await websocket_manager.broadcast({
                    "type": "message",
                    "user_id": user_id,
                    "content": message_data.get("content"),
                    "timestamp": datetime.utcnow().isoformat()
                })
            
            elif message_type == "notification":
                # Aquí se manejarían notificaciones específicas
                pass
    
    except WebSocketDisconnect:
        websocket_manager.disconnect(websocket, user_id)
    except Exception as e:
        print(f"Error en WebSocket para usuario {user_id}: {e}")
        websocket_manager.disconnect(websocket, user_id)

8.3 Frontend: Hook de WebSocket

hooks/useWebSocket.ts:

import { useEffect, useRef, useState, useCallback } from 'react';

interface WebSocketMessage {
  type: string;
  [key: string]: any;
}

interface UseWebSocketOptions {
  onMessage?: (message: WebSocketMessage) => void;
  onOpen?: () => void;
  onClose?: () => void;
  onError?: (error: Event) => void;
  reconnectInterval?: number;
  maxReconnectAttempts?: number;
}

export function useWebSocket(token: string | null, options: UseWebSocketOptions = {}) {
  const {
    onMessage,
    onOpen,
    onClose,
    onError,
    reconnectInterval = 3000,
    maxReconnectAttempts = 5,
  } = options;

  const [isConnected, setIsConnected] = useState(false);
  const [reconnectCount, setReconnectCount] = useState(0);
  const ws = useRef<WebSocket | null>(null);
  const reconnectTimer = useRef<NodeJS.Timeout | null>(null);

  const connect = useCallback(() => {
    if (!token || ws.current?.readyState === WebSocket.OPEN) {
      return;
    }

    const wsUrl = `${process.env.NEXT_PUBLIC_WS_URL}/${token}`;
    console.log('🔌 Conectando WebSocket...');
    
    ws.current = new WebSocket(wsUrl);

    ws.current.onopen = () => {
      console.log('✅ WebSocket conectado');
      setIsConnected(true);
      setReconnectCount(0);
      onOpen?.();
    };

    ws.current.onmessage = (event) => {
      try {
        const message: WebSocketMessage = JSON.parse(event.data);
        console.log('📨 Mensaje recibido:', message);
        onMessage?.(message);
      } catch (error) {
        console.error('Error parseando mensaje:', error);
      }
    };

    ws.current.onclose = () => {
      console.log('🔌 WebSocket desconectado');
      setIsConnected(false);
      onClose?.();

      // Intentar reconectar
      if (reconnectCount < maxReconnectAttempts) {
        reconnectTimer.current = setTimeout(() => {
          console.log(`🔄 Intentando reconectar (${reconnectCount + 1}/${maxReconnectAttempts})...`);
          setReconnectCount(prev => prev + 1);
          connect();
        }, reconnectInterval);
      }
    };

    ws.current.onerror = (error) => {
      console.error('❌ Error en WebSocket:', error);
      onError?.(error);
    };
  }, [token, reconnectCount, onMessage, onOpen, onClose, onError, reconnectInterval, maxReconnectAttempts]);

  // Conectar al montar o cuando cambie el token
  useEffect(() => {
    connect();

    // Cleanup al desmontar
    return () => {
      if (reconnectTimer.current) {
        clearTimeout(reconnectTimer.current);
      }
      if (ws.current) {
        ws.current.close();
      }
    };
  }, [connect]);

  // Función para enviar mensajes
  const sendMessage = useCallback((message: WebSocketMessage) => {
    if (ws.current?.readyState === WebSocket.OPEN) {
      ws.current.send(JSON.stringify(message));
      console.log('📤 Mensaje enviado:', message);
    } else {
      console.warn('⚠️ WebSocket no está conectado');
    }
  }, []);

  // Función para cerrar manualmente
  const disconnect = useCallback(() => {
    if (reconnectTimer.current) {
      clearTimeout(reconnectTimer.current);
    }
    ws.current?.close();
  }, []);

  // Heartbeat (ping/pong)
  useEffect(() => {
    if (!isConnected) return;

    const pingInterval = setInterval(() => {
      sendMessage({ type: 'ping' });
    }, 30000); // cada 30 segundos

    return () => clearInterval(pingInterval);
  }, [isConnected, sendMessage]);

  return {
    isConnected,
    sendMessage,
    disconnect,
    reconnect: connect,
  };
}

8.4 Uso en Componentes

Ejemplo de uso en Dashboard:

'use client';

import { useAuth } from '@/hooks/useAuth';
import { useWebSocket } from '@/hooks/useWebSocket';
import { useEffect, useState } from 'react';
import { toast } from '@/components/ui/use-toast';

export default function Dashboard() {
  const { user } = useAuth();
  const [notifications, setNotifications] = useState<any[]>([]);
  
  // Obtener token (simplificado, en producción usar estado global)
  const [token, setToken] = useState<string | null>(null);
  
  useEffect(() => {
    // Aquí obtendrías el token del estado de autenticación
    // setToken(authToken);
  }, []);

  const { isConnected, sendMessage } = useWebSocket(token, {
    onMessage: (message) => {
      console.log('Mensaje recibido:', message);
      
      switch (message.type) {
        case 'notification':
          setNotifications(prev => [...prev, message]);
          toast({
            title: 'Nueva notificación',
            description: message.content,
          });
          break;
        
        case 'task_updated':
          // Actualizar UI con nueva información de tarea
          break;
        
        case 'project_updated':
          // Actualizar UI con nueva información de proyecto
          break;
      }
    },
    
    onOpen: () => {
      console.log('WebSocket conectado');
      toast({
        title: 'Conectado',
        description: 'Actualizaciones en tiempo real activadas',
      });
    },
    
    onClose: () => {
      console.log('WebSocket desconectado');
    },
  });

  return (
    <div>
      <div className="flex items-center gap-2">
        <div className={`h-2 w-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
        <span>{isConnected ? 'Conectado' : 'Desconectado'}</span>
      </div>
      
      {/* Rest of dashboard */}
    </div>
  );
}

9. AUTENTICACIÓN Y SEGURIDAD

9.1 Flujo de Autenticación Completo

1. REGISTRO
   Usuario → Frontend: Completa formulario de registro
   Frontend: Valida datos localmente
   Frontend → Backend: POST /api/v1/auth/register
   Backend: Valida datos con Pydantic
   Backend: Hash de contraseña con bcrypt
   Backend: Crea usuario en BD
   Backend → Frontend: Usuario creado (sin login automático)
   Frontend: Redirige a página de login

2. LOGIN
   Usuario → Frontend: Ingresa email y contraseña
   Frontend → Backend: POST /api/v1/auth/login (OAuth2PasswordRequestForm)
   Backend: Busca usuario por email
   Backend: Verifica contraseña con bcrypt
   Backend: Genera JWT access token (30 min)
   Backend: Genera JWT refresh token (7 días)
   Backend: Set-Cookie con tokens (HttpOnly, Secure, SameSite=Strict)
   Backend → Frontend: Tokens en cookies + respuesta JSON
   Frontend: Guarda estado de autenticación
   Frontend: Redirige a dashboard

3. ACCESO A RECURSOS PROTEGIDOS
   Frontend → Backend: GET /api/v1/users/me (cookie enviada automáticamente)
   Backend: Extrae token de cookie
   Backend: Valida y decodifica JWT
   Backend: Obtiene user_id del payload
   Backend: Busca usuario en BD
   Backend: Verifica que usuario esté activo
   Backend → Frontend: Datos del usuario
   Frontend: Actualiza UI

4. REFRESH TOKEN (Renovación)
   Frontend: Detecta que access token expiró
   Frontend → Backend: POST /api/v1/auth/refresh (refresh token en cookie)
   Backend: Valida refresh token
   Backend: Genera nuevo access token
   Backend: Set-Cookie con nuevo access token
   Backend → Frontend: Nuevo token
   Frontend: Reintenta request original

5. LOGOUT
   Usuario → Frontend: Click en logout
   Frontend → Backend: POST /api/v1/auth/logout
   Backend: Delete-Cookie (access_token y refresh_token)
   Backend → Frontend: Confirmación
   Frontend: Limpia estado de autenticación
   Frontend: Redirige a login

9.2 Seguridad Implementada

Hashing de Contraseñas

  • Algoritmo: bcrypt
  • Rounds: 12 (por defecto en passlib)
  • Salting: Automático en bcrypt
  • Nunca almacenar contraseñas en texto plano

JSON Web Tokens (JWT)

  • Algoritmo: HS256 (HMAC-SHA256)
  • Secret Key: 256 bits generado aleatoriamente
  • Claims incluidos:
    • sub: User ID
    • iat: Issued At (timestamp)
    • exp: Expiration (timestamp)
    • type: "access" o "refresh"

Cookies Seguras

  • HttpOnly: true - No accesible desde JavaScript
  • Secure: true en producción - Solo HTTPS
  • SameSite: Strict - Previene CSRF
  • Max-Age: Basado en tipo de token

CORS (Cross-Origin Resource Sharing)

  • Origins permitidos configurados explícitamente
  • Credentials: true para permitir cookies
  • Methods: Solo los necesarios
  • Headers: Configurados apropiadamente

Validación de Entrada

  • Backend: Pydantic schemas con validaciones
  • Frontend: Zod + react-hook-form
  • Validación de tipos, longitudes, formatos
  • Sanitización de datos

SQL Injection Prevention

  • SQLAlchemy ORM previene inyección SQL
  • Uso de parámetros preparados
  • Nunca concatenar strings en queries

9.3 Mejores Prácticas de Seguridad

  1. Variables de Entorno

    • Nunca commitear archivos .env
    • Usar secretos diferentes por entorno
    • Rotar secretos periódicamente
  2. HTTPS en Producción

    • Forzar HTTPS
    • HSTS headers
    • Certificados SSL/TLS válidos
  3. Rate Limiting

    • Limitar intentos de login
    • Prevenir brute force attacks
    • Usar herramientas como slowapi
  4. Logging y Monitoreo

    • Log de intentos de autenticación
    • Alertas de actividad sospechosa
    • No logear información sensible
  5. Dependency Updates

    • Mantener dependencias actualizadas
    • Escanear vulnerabilidades (Snyk, Dependabot)
    • Aplicar parches de seguridad

10. MEJORES PRÁCTICAS APLICADAS

10.1 Arquitectura y Diseño

Clean Architecture: Separación clara en capas ✅ SOLID Principles: Código mantenible y extensible ✅ DRY (Don't Repeat Yourself): Reutilización de código ✅ Separation of Concerns: Cada módulo una responsabilidad ✅ Dependency Injection: Código desacoplado y testeable

10.2 Desarrollo

Type Safety: Python type hints + TypeScript ✅ Validación de Datos: Pydantic + Zod ✅ Error Handling: Excepciones personalizadas y manejo centralizado ✅ Async/Await: Operaciones asíncronas para mejor rendimiento ✅ Code Style: Black, isort, Prettier, ESLint

10.3 Base de Datos

ORM: SQLAlchemy para abstracción de BD ✅ Migraciones: Alembic para control de versiones de esquema ✅ Relaciones: Foreign keys, constraints, índices apropiados ✅ Transacciones: Manejo adecuado de commits y rollbacks ✅ Connection Pooling: Reutilización eficiente de conexiones

10.4 Seguridad

Authentication: JWT con cookies seguras ✅ Password Hashing: bcrypt para contraseñas ✅ CORS: Configurado apropiadamente ✅ Input Validation: Validación exhaustiva de entrada ✅ SQL Injection Prevention: ORM con parámetros preparados

10.5 Testing

Unit Tests: Pruebas de componentes individuales ✅ Integration Tests: Pruebas de flujos completos ✅ Mocking: Mockear dependencias externas ✅ Coverage: Medir cobertura de código ✅ CI/CD: Integración continua con tests automáticos

10.6 Documentación

API Documentation: OpenAPI/Swagger automático ✅ Code Comments: Comentarios donde sea necesario ✅ Type Hints: Documentación implícita con tipos ✅ README: Instrucciones claras de instalación y uso ✅ Architecture Docs: Documentación de diseño

10.7 DevOps

Containerization: Docker/Podman para portabilidad ✅ Environment Variables: Configuración externa ✅ Logging: Logs estructurados y útiles ✅ Health Checks: Endpoints de monitoreo ✅ Graceful Shutdown: Manejo apropiado de señales


11. REFERENCIAS CRUZADAS ENTRE EPISODIOS

Video 1 → Videos posteriores

  • Arquitectura presentada → Implementada en Videos 2-5
  • Stack tecnológico → Configurado en Videos 2-3
  • Características clave → Desarrolladas progresivamente

Video 2 → Videos 3, 4, 5

  • Backend básico → Consumido por Frontend (Video 3)
  • Estructura inicial → Refactorizada (Video 4)
  • SQLAlchemy setup → Modelos creados (Video 5)

Video 3 → Videos 4, 5

  • Frontend configurado → Integrado con Backend mejorado
  • Cliente API → Consume endpoints expandidos
  • Componentes UI → Utilizan datos de modelos

Video 4 → Video 5

  • Arquitectura en capas → Facilita implementación de modelos
  • Repositorios → Extendidos para nuevas entidades
  • Servicios → Implementan lógica de modelos complejos

Video 5 → Siguientes episodios

  • Modelos completos → Base para CRUD completo
  • Relaciones → Queries complejas y joins
  • Migraciones → Esquema versionado y actualizable

Progresión Natural del Curso

1. Visión General (Video 1)
   ↓
2. Backend Básico (Video 2)
   ↓
3. Frontend Básico (Video 3)
   ↓
4. Refactorización Profesional (Video 4)
   ↓
5. Modelos y Relaciones Complejas (Video 5)
   ↓
[Episodios Futuros]
   ↓
6. CRUD Completo para todas las entidades
7. Implementación completa de WebSockets
8. Sistema de autenticación completo
9. Testing comprehensivo
10. Despliegue en producción

📚 RECURSOS ADICIONALES

Documentación Oficial

  • FastAPI: https://fastapi.tiangolo.com/
  • Next.js: https://nextjs.org/docs
  • PostgreSQL: https://www.postgresql.org/docs/
  • SQLAlchemy: https://docs.sqlalchemy.org/
  • Pydantic: https://docs.pydantic.dev/
  • Docker: https://docs.docker.com/

Herramientas

  • Swagger Editor: https://editor.swagger.io/
  • Postman: https://www.postman.com/
  • DBeaver: https://dbeaver.io/ (cliente PostgreSQL)
  • VS Code Extensions:
    • Python
    • Pylance
    • ES7+ React/Redux/React-Native snippets
    • Tailwind CSS IntelliSense

Comunidad

  • FastAPI Discord: https://discord.gg/VQjSZaeJmf
  • Next.js Discord: https://nextjs.org/discord
  • Stack Overflow: Etiquetas #fastapi #nextjs

Fin del Documento 2

--------------------------------------------------


DOCUMENTO 3: RESUMEN ESTRUCTURADO POR EPISODIO

DOCUMENTO 3: RESUMEN ESTRUCTURADO POR EPISODIO

Serie: Aplicación Full Stack con FastAPI y Next.js

Canal: Código para Principiantes


📋 ÍNDICE DE EPISODIOS

  1. Episodio 1: Introducción al Proyecto Full Stack
  2. Episodio 2: Preparación del Backend con FastAPI
  3. Episodio 3: Configuración del Frontend con Next.js
  4. Episodio 4: Reorganización Profesional del Backend
  5. Episodio 5: Creación de Modelos y Relaciones

EPISODIO 1: Introducción al Proyecto Full Stack

📊 Información del Video

  • Título: FastAPI y Next.js: Crea una App Full Stack Real (con WebSockets)
  • URL: https://www.youtube.com/watch?v=5oNktUuWBMY
  • Duración: 10:13 minutos
  • Vistas: 650
  • Publicado: Hace 3 meses

📝 Resumen Ejecutivo

Este episodio inaugural establece las bases del proyecto Full Stack, presentando la visión general de la aplicación que se construirá a lo largo de la serie. El instructor introduce el stack tecnológico completo (FastAPI, Next.js, PostgreSQL y Podman), explica la arquitectura del sistema y destaca las características principales que hacen de este un proyecto profesional y real, no solo un tutorial básico.

El enfoque está en proporcionar un roadmap claro del curso, motivar a los estudiantes mostrando el alcance del proyecto final, y establecer expectativas sobre lo que aprenderán: desde la configuración básica hasta la implementación de características avanzadas como WebSockets y autenticación segura con JWT.

🎯 Objetivos del Episodio

Objetivo Principal: Presentar la visión completa del proyecto y el plan de la serie

Objetivos Específicos:

  1. ✅ Introducir el stack tecnológico (FastAPI + Next.js + PostgreSQL + Podman)
  2. ✅ Explicar la arquitectura general de la aplicación
  3. ✅ Destacar características clave: WebSockets y autenticación segura
  4. ✅ Motivar el aprendizaje mostrando un proyecto real
  5. ✅ Establecer el roadmap de episodios futuros

🔑 Puntos Clave Técnicos

1. Stack Tecnológico Presentado

Backend: FastAPI

  • Framework Python moderno y de alto rendimiento
  • Basado en estándares: OpenAPI y JSON Schema
  • Documentación automática (Swagger UI)
  • Validación de datos con Pydantic
  • Soporte nativo para async/await
  • Ventaja principal: Desarrollo rápido con alta calidad de código

Frontend: Next.js

  • Framework React con Server-Side Rendering
  • Enrutamiento automático basado en archivos
  • Optimización de performance out-of-the-box
  • Excelente experiencia de desarrollo
  • Ventaja principal: SEO mejorado y carga inicial rápida

Base de Datos: PostgreSQL

  • Sistema de gestión de BD relacional robusto
  • ACID compliance para transacciones confiables
  • Extensible y con gran ecosistema
  • Ventaja principal: Confiabilidad en producción

Contenedorización: Podman

  • Alternativa a Docker sin daemon root
  • Mayor seguridad al no requerir privilegios de root
  • Compatible con comandos de Docker
  • Ventaja principal: Seguridad mejorada en desarrollo y producción

2. Arquitectura del Sistema

Modelo de Tres Capas:

[Frontend - Next.js] ← HTTP/WebSocket → [Backend - FastAPI] ← SQL → [DB - PostgreSQL]
  • Capa de Presentación: Next.js maneja UI/UX
  • Capa de Lógica de Negocio: FastAPI procesa requests y lógica
  • Capa de Datos: PostgreSQL persiste información

Comunicación:

  • REST API para operaciones CRUD
  • WebSockets para actualizaciones en tiempo real
  • JWT en cookies HttpOnly para autenticación

3. Características Destacadas

WebSockets para Tiempo Real:

  • Comunicación bidireccional y persistente
  • Ideal para notificaciones instantáneas
  • Sincronización de datos entre clientes
  • Baja latencia comparado con polling

Autenticación Segura:

  • JSON Web Tokens (JWT) para stateless auth
  • Almacenamiento en cookies HttpOnly (no accesible desde JS)
  • Secure flag para transmisión solo por HTTPS
  • SameSite=Strict para prevenir CSRF

Containerización Completa:

  • Desarrollo en contenedores = producción sin sorpresas
  • Aislamiento de dependencias
  • Fácil escalabilidad horizontal
  • Reproducibilidad del entorno

💼 Tecnologías Utilizadas

Categoría Tecnología Versión Propósito
Backend Framework FastAPI Latest API REST asíncrona
Frontend Framework Next.js 14+ Aplicación React con SSR
Base de Datos PostgreSQL 15+ Persistencia relacional
Contenedores Podman/Docker Latest Entorno consistente
Lenguaje Backend Python 3.11+ Lógica del servidor
Lenguaje Frontend TypeScript 5+ Type safety en cliente
ORM SQLAlchemy 2.0 Interacción con BD
Validación Pydantic 2.0+ Schemas y validación

✅ Resultados Alcanzados

Al finalizar este episodio, los estudiantes han logrado:

  1. Comprensión Clara del Proyecto:

    • Entienden qué se va a construir
    • Conocen las tecnologías involucradas
    • Tienen expectativas realistas del alcance
  2. Motivación Establecida:

    • Ven el valor de un proyecto profesional
    • Comprenden las aplicaciones prácticas
    • Están preparados para el compromiso de la serie
  3. Contexto Técnico:

    • Conocen las ventajas de cada tecnología
    • Entienden por qué se eligió este stack
    • Ven cómo las piezas encajan juntas
  4. Roadmap Mental:

    • Saben qué esperar en próximos episodios
    • Entienden la progresión del aprendizaje
    • Pueden planificar su tiempo de estudio

🔗 Conexión con Episodios Previos/Siguientes

No hay episodios previos - Este es el inicio de la serie

Prepara para:

  • Episodio 2: Implementación práctica del backend presentado aquí
  • Episodio 3: Configuración del frontend para consumir el backend
  • Episodios 4-5: Refinamiento y características avanzadas

Conceptos que serán expandidos:

  • WebSockets: Implementación completa en episodios posteriores
  • Autenticación JWT: Código detallado en backend y frontend
  • Arquitectura: Refactorización profesional en episodio 4
  • Base de datos: Modelado completo en episodio 5

📌 Notas Importantes

Para Principiantes: Este episodio es principalmente conceptual. No te preocupes si no entiendes todos los términos técnicos; se explicarán con código en episodios posteriores.

Requisito Previo: Conocimientos básicos de Python y JavaScript son recomendados, pero no estrictamente necesarios.

Tiempo de Compromiso: La serie completa requiere seguimiento constante y práctica. Reserva tiempo para codificar junto con los videos.

🎬 Estructura del Episodio

[00:00-02:00] → Introducción y bienvenida
[02:00-04:00] → Presentación del stack tecnológico
[04:00-06:30] → Arquitectura y diseño del sistema
[06:30-08:30] → Características principales: WebSockets y auth
[08:30-10:13] → Roadmap y conclusión

EPISODIO 2: Preparación del Backend con FastAPI

📊 Información del Video

  • Título: FastAPI y Next.js: Preparamos el Backend | Curso App Full Stack #2
  • URL: https://www.youtube.com/watch?v=fNgmKHZ1HJU
  • Duración: 43:31 minutos ⭐ (El más largo de la serie)
  • Vistas: 538
  • Publicado: Hace 3 meses

📝 Resumen Ejecutivo

Este episodio marca el inicio de la implementación práctica, siendo el más extenso y detallado de la serie. El instructor guía paso a paso la configuración completa del backend con FastAPI, desde la instalación de dependencias hasta la creación de un contenedor Docker funcional. Se establece la estructura base del proyecto, se configura la conexión a PostgreSQL, se implementan endpoints de prueba y se introduce el concepto de documentación automática con Swagger UI.

Es un episodio fundamental que sienta las bases técnicas sobre las cuales se construirá todo el backend. Se presta especial atención a las mejores prácticas desde el inicio, como el uso de variables de entorno, la organización modular del código y la configuración apropiada del servidor ASGI con Uvicorn.

🎯 Objetivos del Episodio

Objetivo Principal: Configurar un backend FastAPI funcional y profesional desde cero

Objetivos Específicos:

  1. ✅ Instalar Python 3.11+ y crear entorno virtual
  2. ✅ Instalar FastAPI, Uvicorn y todas las dependencias necesarias
  3. ✅ Crear la estructura inicial del proyecto backend
  4. ✅ Configurar PostgreSQL y establecer conexión con SQLAlchemy
  5. ✅ Implementar configuración con variables de entorno
  6. ✅ Crear la aplicación FastAPI con middleware CORS
  7. ✅ Implementar endpoints básicos de prueba y health check
  8. ✅ Configurar Dockerfile y docker-compose para el backend
  9. ✅ Verificar funcionamiento con Swagger UI

🔑 Puntos Clave Técnicos

1. Instalación y Configuración del Entorno

Creación de Entorno Virtual:

python -m venv venv
source venv/bin/activate  # Linux/Mac
venv\Scripts\activate     # Windows
  • Aislamiento de dependencias del sistema
  • Evita conflictos entre proyectos
  • Facilita deployment reproducible

Dependencias Principales Instaladas:

  • fastapi: Framework principal
  • uvicorn[standard]: Servidor ASGI con extras
  • sqlalchemy: ORM para PostgreSQL
  • psycopg2-binary: Driver de PostgreSQL
  • alembic: Migraciones de base de datos
  • pydantic-settings: Configuración con validación
  • python-jose[cryptography]: JWT
  • passlib[bcrypt]: Hashing de contraseñas
  • python-dotenv: Manejo de .env files

2. Estructura del Proyecto Backend

Organización Inicial:

backend/
├── app/
│   ├── __init__.py          # Marca como paquete Python
│   ├── main.py              # Punto de entrada
│   ├── config.py            # Settings centralizados
│   ├── api/
│   │   ├── __init__.py
│   │   └── routes.py        # Endpoints iniciales
│   ├── core/
│   │   ├── __init__.py
│   │   └── security.py      # JWT y hashing (futuro)
│   └── db/
│       ├── __init__.py
│       └── database.py      # Conexión a BD
├── requirements.txt
├── .env
└── Dockerfile

Razones de esta estructura:

  • Modularidad desde el inicio
  • Fácil de entender y navegar
  • Escalable para crecer
  • Sigue convenciones de Python

3. Configuración de PostgreSQL y SQLAlchemy

Conexión a Base de Datos:

DATABASE_URL = "postgresql://usuario:contraseña@host:puerto/database"

engine = create_engine(
    DATABASE_URL,
    pool_pre_ping=True,    # Verifica conexiones antes de usar
    pool_size=10,          # Máximo 10 conexiones simultáneas
    max_overflow=20        # Hasta 20 adicionales si necesario
)

Session Management:

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()  # Siempre cerrar sesión

Pool de Conexiones:

  • Reutiliza conexiones existentes
  • Mejora performance significativamente
  • Evita overhead de crear/cerrar conexiones

4. Variables de Entorno y Configuración

Archivo .env:

APP_NAME="FastAPI Full Stack"
DEBUG=True
DATABASE_URL=postgresql://fastapi_user:password@localhost:5432/fullstack_db
SECRET_KEY=tu-clave-super-secreta

Pydantic Settings:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    APP_NAME: str
    DEBUG: bool = False
    DATABASE_URL: str
    SECRET_KEY: str
    
    class Config:
        env_file = ".env"

Beneficios:

  • Separación de configuración del código
  • Diferentes configs por entorno (dev/prod)
  • Validación automática con Pydantic
  • Type hints para mejor DX

5. Aplicación FastAPI Principal

Creación de la App:

app = FastAPI(
    title="FastAPI Full Stack",
    version="1.0.0",
    docs_url="/docs",      # Swagger UI
    redoc_url="/redoc"     # ReDoc
)

CORS Configuration:

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],  # Next.js
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
  • Permite requests desde Next.js
  • allow_credentials=True para cookies
  • Configuración permisiva en desarrollo

Health Check Endpoint:

@app.get("/health")
async def health_check():
    return {"status": "healthy", "version": "1.0.0"}
  • Útil para monitoring y load balancers
  • Verifica que el servidor esté respondiendo

6. Contenedorización con Docker

Dockerfile:

FROM python:3.11-slim

WORKDIR /app

# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \
    gcc postgresql-client

# Instalar dependencias Python
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copiar código
COPY ./app ./app

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

docker-compose.yml (parcial):

services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: fastapi_user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: fullstack_db
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
  
  backend:
    build: ./backend
    ports:
      - "8000:8000"
    depends_on:
      - postgres
    volumes:
      - ./backend/app:/app/app  # Hot reload

Ventajas de Containerización:

  • Entorno idéntico en todos los sistemas
  • No contamina instalación local
  • Fácil de compartir y replicar
  • Preparado para producción

💼 Tecnologías Utilizadas

Componente Tecnología Propósito Específico
Framework FastAPI 0.109+ API REST asíncrona
Servidor ASGI Uvicorn Servidor de alto rendimiento
ORM SQLAlchemy 2.0 Abstracción de base de datos
BD Driver psycopg2 Conexión a PostgreSQL
Migraciones Alembic Control de versiones de esquema
Validación Pydantic 2.5+ Schemas y settings
Contenedores Docker/Podman Entorno reproducible
Variables Entorno python-dotenv Configuración externa

✅ Resultados Alcanzados

Al finalizar este episodio, se tiene:

  1. Backend Funcional:

    • Servidor FastAPI corriendo en puerto 8000
    • Documentación automática accesible en /docs
    • Health check endpoint respondiendo
  2. Base de Datos Configurada:

    • PostgreSQL ejecutándose en contenedor
    • Conexión establecida con SQLAlchemy
    • Pool de conexiones configurado
  3. Estructura Profesional:

    • Código organizado en módulos
    • Variables de entorno separadas
    • Configuración centralizada con Pydantic
  4. Entorno Contenerizado:

    • Docker Compose orquestando servicios
    • Volúmenes para persistencia de datos
    • Hot reload para desarrollo ágil
  5. Middleware Configurado:

    • CORS listo para Next.js
    • Preparado para agregar más middleware
  6. Documentación Viva:

    • Swagger UI generado automáticamente
    • Esquemas Pydantic documentando API
    • ReDoc como alternativa

🔗 Conexión con Episodios Previos/Siguientes

Depende de:

  • Episodio 1: Arquitectura y tecnologías presentadas

Prepara para:

  • Episodio 3: Frontend consumirá esta API
  • Episodio 4: Esta estructura será refactorizada profesionalmente
  • Episodio 5: Modelos SQLAlchemy se crearán aquí

Conceptos que se expandirán:

  • Endpoints básicos → CRUD completo
  • Estructura simple → Arquitectura en capas
  • Sin autenticación → JWT completo
  • Sin modelos → Modelos complejos con relaciones

📌 Notas Importantes

Comando Crítico: Siempre activar el entorno virtual antes de trabajar con el proyecto.

Puerto 8000: Asegúrate de que este puerto esté libre en tu sistema.

Variables de Entorno: NUNCA commitear el archivo .env al repositorio. Usar .env.example como plantilla.

Hot Reload: El flag --reload de Uvicorn reinicia automáticamente al detectar cambios en el código.

PostgreSQL: Si usas PostgreSQL local en lugar de Docker, ajusta el host en DATABASE_URL a localhost en lugar de postgres.

🎬 Estructura del Episodio

[00:00-05:00] → Introducción y repaso del ep. 1
[05:00-10:00] → Instalación de Python, venv y FastAPI
[10:00-18:00] → Creación de estructura del proyecto
[18:00-25:00] → Configuración de PostgreSQL y SQLAlchemy
[25:00-32:00] → Implementación de main.py y endpoints
[32:00-38:00] → Dockerfile y docker-compose
[38:00-43:31] → Testing, Swagger UI y conclusiones

💡 Tips del Instructor

  1. Sobre Uvicorn: El flag --reload es solo para desarrollo, nunca en producción
  2. Sobre Pool de Conexiones: Ajustar pool_size según carga esperada
  3. Sobre CORS: En producción, especificar origins exactos, no usar "*"
  4. Sobre .env: Generar SECRET_KEY con openssl rand -hex 32
  5. Sobre Docker: Usar volúmenes nombrados para persistencia importante

EPISODIO 3: Configuración del Frontend con Next.js

📊 Información del Video

  • Título: FastAPI y NextJS: Preparamos el contenedor del Frontend | Curso App Full Stack #3
  • URL: https://www.youtube.com/watch?v=5zkla8UaiwI
  • Duración: 28:14 minutos
  • Vistas: 258
  • Publicado: Hace 3 meses

📝 Resumen Ejecutivo

Este episodio se enfoca completamente en el frontend, configurando Next.js con todas las herramientas modernas necesarias para una aplicación profesional. El instructor guía la creación del proyecto Next.js con App Router, la configuración de TypeScript para type safety, la integración de TailwindCSS para estilos, y la instalación de shadcn/ui para componentes reutilizables.

Se implementa un cliente API con axios configurado para comunicarse con el backend FastAPI, se estructuran los componentes de manera escalable, y se containeriza todo en Docker. El episodio termina con la aplicación frontend conectándose exitosamente al backend y lista para recibir características.

🎯 Objetivos del Episodio

Objetivo Principal: Configurar un frontend Next.js moderno y profesional que se comunique con el backend FastAPI

Objetivos Específicos:

  1. ✅ Crear proyecto Next.js con App Router y TypeScript
  2. ✅ Configurar TailwindCSS para estilos utility-first
  3. ✅ Instalar y configurar shadcn/ui para componentes
  4. ✅ Estructurar el proyecto con buenas prácticas
  5. ✅ Implementar cliente API con axios
  6. ✅ Configurar variables de entorno para URLs
  7. ✅ Crear contenedor Docker para Next.js
  8. ✅ Integrar frontend y backend en docker-compose
  9. ✅ Verificar comunicación frontend-backend

🔑 Puntos Clave Técnicos

1. Creación del Proyecto Next.js

Comando de Inicialización:

npx create-next-app@latest frontend \
  --typescript \
  --tailwind \
  --app \
  --src-dir \
  --no-git

Opciones Seleccionadas:

  • --typescript: Type safety con TypeScript
  • --tailwind: TailwindCSS preconfigurado
  • --app: App Router (nueva arquitectura de Next.js)
  • --src-dir: Código en carpeta src/
  • --no-git: No inicializar Git (ya está en raíz del proyecto)

Ventajas del App Router:

  • Server Components por defecto (mejor performance)
  • Layouts anidados
  • Loading y error states más fáciles
  • Streaming y Suspense integrados

2. TypeScript Configuration

tsconfig.json Configurado:

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "paths": {
      "@/*": ["./src/*"]  // Path aliases
    }
  }
}

Beneficios de TypeScript:

  • Detección de errores en tiempo de desarrollo
  • IntelliSense mejorado en VSCode
  • Refactoring más seguro
  • Documentación implícita con tipos
  • Prevención de bugs comunes

3. TailwindCSS y Estilos

tailwind.config.ts:

import type { Config } from "tailwindcss"

const config: Config = {
  darkMode: ["class"],  // Dark mode con clase
  content: [
    './pages/**/*.{ts,tsx}',
    './components/**/*.{ts,tsx}',
    './app/**/*.{ts,tsx}',
    './src/**/*.{ts,tsx}',
  ],
  theme: {
    extend: {
      colors: {
        border: "hsl(var(--border))",
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        primary: { /* ... */ },
        secondary: { /* ... */ },
      },
      borderRadius: {
        lg: "var(--radius)",
        md: "calc(var(--radius) - 2px)",
        sm: "calc(var(--radius) - 4px)",
      },
    },
  },
}

Ventajas de Tailwind:

  • Desarrollo rápido con utility classes
  • No hay CSS no utilizado en producción (tree-shaking)
  • Diseño consistente con sistema de diseño
  • Responsive design sencillo
  • Dark mode fácil de implementar

4. Shadcn/UI Components

Instalación:

npx shadcn-ui@latest init

# Instalar componentes específicos
npx shadcn-ui@latest add button
npx shadcn-ui@latest add input
npx shadcn-ui@latest add card
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add form

Características de shadcn/ui:

  • Componentes copiados al proyecto (no npm package)
  • Totalmente personalizables
  • Accesibles (a11y) por defecto
  • Basados en Radix UI
  • Estilos con Tailwind

Ejemplo de Uso:

import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"

export function MyComponent() {
  return (
    <Card>
      <Button variant="default">Click me</Button>
      <Button variant="outline">Outline</Button>
    </Card>
  )
}

5. Estructura del Proyecto

Organización de Carpetas:

frontend/src/
├── app/                    # App Router
│   ├── layout.tsx         # Root layout
│   ├── page.tsx           # Home page
│   ├── (auth)/            # Grupo de rutas auth
│   │   ├── login/
│   │   │   └── page.tsx
│   │   └── register/
│   │       └── page.tsx
│   └── (protected)/       # Rutas protegidas
│       └── dashboard/
│           └── page.tsx
├── components/
│   ├── ui/                # shadcn components
│   ├── layout/            # Header, Footer, etc.
│   └── forms/             # Formularios
├── lib/
│   ├── api.ts            # Cliente API
│   ├── auth.ts           # Lógica auth
│   └── utils.ts          # Utilidades
├── hooks/
│   ├── useAuth.ts
│   └── useWebSocket.ts
├── types/
│   └── api.ts            # Tipos TypeScript
└── styles/
    └── globals.css       # Estilos globales

Razones de esta Estructura:

  • app/: Rutas según convención de Next.js
  • components/: Reutilización de componentes
  • lib/: Lógica compartida y utilidades
  • hooks/: Custom React hooks
  • types/: Definiciones TypeScript centralizadas

6. Cliente API con Axios

lib/api.ts:

import axios, { AxiosInstance } from 'axios';

const apiClient: AxiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  timeout: 10000,
  withCredentials: true,  // Enviar cookies
  headers: {
    'Content-Type': 'application/json',
  },
});

// Interceptor para manejar errores
apiClient.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export const api = {
  auth: {
    login: async (email: string, password: string) => {
      const formData = new FormData();
      formData.append('username', email);
      formData.append('password', password);
      return apiClient.post('/auth/login', formData);
    },
    // ...más métodos
  },
  // ...más endpoints
};

Características del Cliente:

  • withCredentials: true para enviar/recibir cookies
  • Interceptors para manejo global de errores
  • Timeout configurado para prevenir requests colgados
  • Redireccionamiento automático a login en 401

7. Variables de Entorno

.env.local:

NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
NEXT_PUBLIC_WS_URL=ws://localhost:8000/ws

Uso en Código:

const apiUrl = process.env.NEXT_PUBLIC_API_URL;

Nota Importante:

  • Prefijo NEXT_PUBLIC_ para variables accesibles en el cliente
  • Sin prefijo solo están disponibles en server-side code

8. Containerización del Frontend

Dockerfile (multi-stage):

# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# Stage 2: Builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Stage 3: Runner
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]

next.config.js:

const nextConfig = {
  output: 'standalone',  // Para Docker
  images: {
    domains: ['localhost'],
  },
}

docker-compose.yml Actualizado:

services:
  postgres:
    # ... (mismo que antes)
  
  backend:
    # ... (mismo que antes)
  
  frontend:
    build: ./frontend
    container_name: fullstack_frontend
    environment:
      - NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
    ports:
      - "3000:3000"
    depends_on:
      - backend
    volumes:
      - ./frontend/src:/app/src  # Hot reload

💼 Tecnologías Utilizadas

Componente Tecnología Propósito
Framework Next.js 14 Aplicación React con SSR
Lenguaje TypeScript 5 Type safety
Estilos TailwindCSS 3 Utility-first CSS
Componentes UI shadcn/ui Componentes preconstruidos
HTTP Client Axios Requests a API
Validación Forms Zod + react-hook-form Formularios robustos
Contenedores Docker Entorno consistente

✅ Resultados Alcanzados

Al finalizar este episodio:

  1. Frontend Funcional:

    • Next.js corriendo en puerto 3000
    • App Router configurado
    • TypeScript funcionando
  2. Estilos Configurados:

    • TailwindCSS listo para usar
    • Sistema de diseño con variables CSS
    • Dark mode preparado
  3. Componentes UI:

    • shadcn/ui instalado
    • Botones, inputs, cards, etc. disponibles
    • Personalizables y accesibles
  4. Comunicación con Backend:

    • Cliente API configurado con axios
    • Variables de entorno para URLs
    • Interceptors para manejo de errores
  5. Estructura Escalable:

    • Carpetas organizadas lógicamente
    • Separación de concerns
    • Fácil de navegar y extender
  6. Containerizado:

    • Dockerfile multi-stage optimizado
    • Integrado en docker-compose
    • Hot reload para desarrollo

🔗 Conexión con Episodios Previos/Siguientes

Depende de:

  • Episodio 1: Arquitectura general
  • Episodio 2: Backend FastAPI funcionando

Prepara para:

  • Episodio 4-5: Frontend consumirá nuevos endpoints
  • Episodios futuros: Implementación de features (auth, CRUD, WebSockets)

Conceptos que se usarán:

  • Cliente API → Consumirá todos los endpoints del backend
  • Estructura de componentes → Se llenará con funcionalidad
  • Hooks personalizados → useAuth, useWebSocket, etc.

📌 Notas Importantes

App Router vs Pages Router: Este proyecto usa App Router (recomendado por Next.js). Es diferente a tutoriales antiguos.

Puerto 3000: Asegúrate de que esté libre en tu sistema.

NEXT_PUBLIC_ Prefix: Variables sin este prefijo no estarán disponibles en el cliente.

Hot Reload: Los cambios en src/ se reflejan automáticamente gracias al volumen de Docker.

Build Time: El primer build puede tardar varios minutos mientras descarga dependencias.

🎬 Estructura del Episodio

[00:00-03:00] → Introducción y repaso
[03:00-08:00] → Crear proyecto Next.js y configuración inicial
[08:00-12:00] → Configurar TypeScript y TailwindCSS
[12:00-18:00] → Instalar shadcn/ui y estructurar componentes
[18:00-22:00] → Implementar cliente API
[22:00-26:00] → Containerización con Docker
[26:00-28:14] → Pruebas y conclusión

💡 Tips del Instructor

  1. Sobre shadcn/ui: Los componentes se copian al proyecto, así que puedes modificarlos libremente
  2. Sobre TypeScript: Usar interface para objetos públicos, type para uniones y aliases
  3. Sobre Tailwind: Usar el plugin de VSCode para autocompletado de clases
  4. Sobre API Client: Centralizar todas las llamadas en lib/api.ts facilita mantenimiento
  5. Sobre Hot Reload: Si no funciona, reiniciar el contenedor resuelve la mayoría de problemas

EPISODIO 4: Reorganización Profesional del Backend

📊 Información del Video

  • Título: FastAPI y NextJS: Reorganizamos profesionalmente el Backend | Curso App Full Stack #4
  • URL: https://www.youtube.com/watch?v=4c1Tw2rb5cE
  • Duración: 16:14 minutos
  • Vistas: 142
  • Publicado: Hace 2 meses

📝 Resumen Ejecutivo

Este episodio es crucial para la evolución del proyecto hacia código de producción. El instructor refactoriza completamente el backend, transformándolo de una estructura básica a una arquitectura profesional en capas. Se implementan patrones de diseño fundamentales como Repository Pattern y Service Layer, se introducen excepciones personalizadas, y se establece un sistema robusto de inyección de dependencias.

Aunque más corto que episodios anteriores, este es denso en conceptos de arquitectura de software. El enfoque no está tanto en escribir mucho código nuevo, sino en reorganizar el existente de manera que sea mantenible, testeable y escalable. Es un episodio que separa proyectos de tutorial de aplicaciones reales.

🎯 Objetivos del Episodio

Objetivo Principal: Transformar el backend básico en una arquitectura profesional y escalable

Objetivos Específicos:

  1. ✅ Implementar arquitectura en capas (API → Services → Repositories → Models)
  2. ✅ Crear patrón Repository con CRUD genérico
  3. ✅ Implementar Service Layer para lógica de negocio
  4. ✅ Definir excepciones personalizadas
  5. ✅ Mejorar sistema de dependencias (deps.py)
  6. ✅ Versionar la API (prefijo /api/v1)
  7. ✅ Refactorizar endpoints para usar nueva arquitectura
  8. ✅ Preparar estructura para crecer sin fricción

🔑 Puntos Clave Técnicos

1. Arquitectura en Capas Implementada

Separación de Responsabilidades:

┌─────────────────────────────────────────┐
│   API LAYER (api/v1/endpoints/)        │
│   • Recibe requests HTTP                │
│   • Valida datos con Pydantic          │
│   • Llama a servicios                   │
│   • Serializa respuestas                │
└───────────────┬─────────────────────────┘
                │ Depends
┌───────────────▼─────────────────────────┐
│   SERVICE LAYER (services/)             │
│   • Lógica de negocio                   │
│   • Validaciones complejas              │
│   • Orquesta repositorios               │
│   • Maneja transacciones                │
└───────────────┬─────────────────────────┘
                │ Usa
┌───────────────▼─────────────────────────┐
│   REPOSITORY LAYER (repositories/)      │
│   • Acceso a datos (CRUD)              │
│   • Queries a base de datos            │
│   • Abstrae SQLAlchemy                  │
│   • Sin lógica de negocio              │
└───────────────┬─────────────────────────┘
                │ Opera sobre
┌───────────────▼─────────────────────────┐
│   MODEL LAYER (models/)                 │
│   • Definición de tablas                │
│   • Relaciones entre entidades          │
│   • Constraints y validaciones BD       │
└─────────────────────────────────────────┘

Beneficios de esta Arquitectura:

  • Cada capa tiene responsabilidad única (SRP)
  • Testing más fácil (mockear cada capa)
  • Cambios localizados (modificar una capa sin afectar otras)
  • Reutilización de código
  • Escalabilidad vertical y horizontal

2. Repository Pattern (Patrón Repositorio)

BaseRepository Genérico:

from typing import TypeVar, Generic, Type, Optional, List
from sqlalchemy.orm import Session

ModelType = TypeVar("ModelType")

class BaseRepository(Generic[ModelType]):
    def __init__(self, model: Type[ModelType]):
        self.model = model
    
    def get(self, db: Session, id: int) -> Optional[ModelType]:
        return db.query(self.model).filter(self.model.id == id).first()
    
    def get_multi(self, db: Session, skip: int = 0, limit: int = 100) -> List[ModelType]:
        return db.query(self.model).offset(skip).limit(limit).all()
    
    def create(self, db: Session, *, obj_in: dict) -> ModelType:
        db_obj = self.model(**obj_in)
        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj
    
    def update(self, db: Session, *, db_obj: ModelType, obj_in: dict) -> ModelType:
        for field, value in obj_in.items():
            setattr(db_obj, field, value)
        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj
    
    def delete(self, db: Session, *, id: int) -> Optional[ModelType]:
        obj = db.query(self.model).get(id)
        if obj:
            db.delete(obj)
            db.commit()
        return obj

UserRepository Específico:

from app.models.user import User

class UserRepository(BaseRepository[User]):
    def __init__(self):
        super().__init__(User)
    
    def get_by_email(self, db: Session, *, email: str) -> Optional[User]:
        return db.query(User).filter(User.email == email).first()
    
    def get_by_username(self, db: Session, *, username: str) -> Optional[User]:
        return db.query(User).filter(User.username == username).first()

user_repository = UserRepository()  # Instancia global

Ventajas del Repository Pattern:

  • CRUD genérico reutilizable
  • Fácil de mockear para testing
  • Abstrae detalles de SQLAlchemy
  • Queries específicas en repositorios especializados
  • Cambiar ORM afecta solo repositorios

3. Service Layer (Capa de Servicios)

UserService con Lógica de Negocio:

class UserService:
    def __init__(self):
        self.repo = user_repository
    
    def create_user(self, db: Session, user_in: UserCreate) -> User:
        # Validación: email único
        if self.repo.get_by_email(db, email=user_in.email):
            raise UserAlreadyExistsException("Email ya registrado")
        
        # Validación: username único
        if self.repo.get_by_username(db, username=user_in.username):
            raise UserAlreadyExistsException("Username ya existe")
        
        # Hash de contraseña
        hashed_password = get_password_hash(user_in.password)
        
        # Crear usuario
        user_data = user_in.dict(exclude={'password'})
        user_data['hashed_password'] = hashed_password
        
        return self.repo.create(db, obj_in=user_data)
    
    def authenticate(self, db: Session, *, email: str, password: str) -> Optional[User]:
        user = self.repo.get_by_email(db, email=email)
        if not user:
            return None
        if not verify_password(password, user.hashed_password):
            return None
        return user

user_service = UserService()  # Instancia global

Responsabilidades del Service Layer:

  • Implementar reglas de negocio
  • Coordinar múltiples repositorios
  • Validaciones complejas
  • Transformación de datos
  • Manejo de transacciones
  • No acceder directamente a la BD

4. Excepciones Personalizadas

core/exceptions.py:

from fastapi import HTTPException, status

class BaseAPIException(HTTPException):
    def __init__(self, detail: str, status_code: int = 400):
        super().__init__(status_code=status_code, detail=detail)

class UserAlreadyExistsException(BaseAPIException):
    def __init__(self, detail: str = "El usuario ya existe"):
        super().__init__(detail, status_code=status.HTTP_409_CONFLICT)

class UserNotFoundException(BaseAPIException):
    def __init__(self, detail: str = "Usuario no encontrado"):
        super().__init__(detail, status_code=status.HTTP_404_NOT_FOUND)

class InvalidCredentialsException(BaseAPIException):
    def __init__(self, detail: str = "Credenciales inválidas"):
        super().__init__(detail, status_code=status.HTTP_401_UNAUTHORIZED)

Ventajas:

  • Errores consistentes en toda la API
  • Códigos de estado HTTP apropiados
  • Mensajes descriptivos
  • Fácil de extender

5. Sistema de Dependencias Mejorado

api/deps.py Refactorizado:

from fastapi import Depends, HTTPException, status, Cookie
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from app.db.session import SessionLocal
from app.models.user import User
from app.services.user_service import user_service
from app.core.security import decode_token

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login", auto_error=False)

def get_db() -> Generator:
    """Dependency de base de datos"""
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

async def get_current_user(
    db: Session = Depends(get_db),
    token: Optional[str] = Depends(oauth2_scheme),
    access_token: Optional[str] = Cookie(None)
) -> User:
    """Obtener usuario actual desde JWT"""
    # Priorizar cookie sobre header
    token_to_decode = access_token or token
    
    if not token_to_decode:
        raise HTTPException(status_code=401, detail="No autenticado")
    
    payload = decode_token(token_to_decode)
    if not payload:
        raise HTTPException(status_code=401, detail="Token inválido")
    
    user_id = int(payload.get("sub"))
    user = user_service.get_user(db, user_id=user_id)
    
    if not user:
        raise HTTPException(status_code=401, detail="Usuario no encontrado")
    
    return user

async def get_current_active_user(
    current_user: User = Depends(get_current_user),
) -> User:
    """Verificar usuario activo"""
    if not current_user.is_active:
        raise HTTPException(status_code=400, detail="Usuario inactivo")
    return current_user

Composición de Dependencias:

  • get_current_active_user depende de get_current_user
  • get_current_user depende de get_db
  • FastAPI resuelve automáticamente el árbol de dependencias

6. Versionado de API

Estructura Versionada:

app/api/
├── deps.py
└── v1/
    ├── router.py          # Agrega todos los routers
    └── endpoints/
        ├── auth.py        # POST /api/v1/auth/login
        ├── users.py       # GET /api/v1/users/
        ├── projects.py    # GET /api/v1/projects/
        └── tasks.py       # GET /api/v1/tasks/

Router Principal Versionado:

# app/api/v1/router.py
from fastapi import APIRouter
from app.api.v1.endpoints import auth, users, projects

api_router = APIRouter()

api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(projects.router, prefix="/projects", tags=["projects"])

En main.py:

app.include_router(api_router, prefix="/api/v1")

Beneficios del Versionado:

  • Cambios breaking en v2 sin afectar v1
  • Clientes pueden migrar gradualmente
  • Deprecar versiones antiguas de forma controlada
  • Profesionalismo y estándares de API

7. Endpoints Refactorizados

Antes (Video 2):

@app.post("/register")
def register(user_in: UserCreate, db: Session = Depends(get_db)):
    # Validación inline
    if db.query(User).filter(User.email == user_in.email).first():
        raise HTTPException(400, "Email ya existe")
    # Hash inline
    hashed = pwd_context.hash(user_in.password)
    # Creación inline
    user = User(**user_in.dict(), hashed_password=hashed)
    db.add(user)
    db.commit()
    return user

Después (Video 4):

@router.post("/register", response_model=UserResponse, status_code=201)
def register(
    user_in: UserCreate,
    db: Session = Depends(deps.get_db)
):
    """Registrar nuevo usuario"""
    return auth_service.register_user(db, user_in=user_in)

Mejoras Evidentes:

  • Endpoint limpio y conciso
  • Toda la lógica en el servicio
  • Fácil de testear
  • Exceptions manejadas centralmente
  • Type hints y response_model

💼 Tecnologías y Patrones Utilizados

Patrón/Concepto Implementación Beneficio Principal
Repository Pattern BaseRepository genérico Reutilización CRUD
Service Layer UserService, AuthService Lógica de negocio centralizada
Dependency Injection FastAPI Depends Código desacoplado
Exception Handling Excepciones personalizadas Errores consistentes
API Versioning /api/v1/ prefix Evolución sin breaking changes
Generic Types TypeVar en repositories Type safety con flexibilidad

✅ Resultados Alcanzados

Al finalizar este episodio:

  1. Arquitectura Profesional:

    • 4 capas bien definidas
    • Separación clara de responsabilidades
    • Código mantenible y escalable
  2. Repository Pattern:

    • CRUD genérico funcionando
    • Repositorios específicos por entidad
    • Abstracción completa de SQLAlchemy
  3. Service Layer:

    • Lógica de negocio centralizada
    • Validaciones complejas
    • Servicios reutilizables
  4. Manejo de Errores:

    • Excepciones personalizadas
    • Códigos HTTP apropiados
    • Mensajes de error consistentes
  5. API Versionada:

    • Estructura para versiones futuras
    • Documentación organizada por versión
    • Evolución controlada
  6. Testing Facilitado:

    • Cada capa puede testearse independientemente
    • Mocking sencillo con interfaces claras
    • Cobertura de código más fácil

🔗 Conexión con Episodios Previos/Siguientes

Depende de:

  • Episodio 2: Estructura básica que se refactoriza

Mejora:

  • Episodio 3: Frontend ahora consume API más robusta

Prepara para:

  • Episodio 5: Modelos complejos usan repositorios y servicios
  • Episodios futuros: CRUD completo con arquitectura sólida

Transformación Clave:

Episodio 2: main.py con todo inline
            ↓ Refactorización
Episodio 4: API → Service → Repository → Model

📌 Notas Importantes

Refactoring vs Rewriting: Este episodio refactoriza, no reescribe. El comportamiento externo sigue igual.

Backward Compatibility: Los endpoints mantienen sus URLs (solo agregan prefijo /api/v1/).

No Premature Optimization: Esta arquitectura es necesaria ahora que el proyecto crece, no desde el inicio.

Testing: Aunque no se muestra en este episodio, esta arquitectura facilita enormemente el testing.

Migraciones: No es necesario correr migraciones ya que no cambiaron los modelos, solo la estructura del código.

🎬 Estructura del Episodio

[00:00-02:00] → Introducción y problemas de arquitectura actual
[02:00-05:00] → Diseño de nueva arquitectura en capas
[05:00-08:00] → Implementación de Repository Pattern
[08:00-11:00] → Creación de Service Layer
[11:00-14:00] → Refactorización de endpoints
[14:00-16:14] → Testing y conclusión

💡 Tips del Instructor

  1. Sobre Refactoring: Hacer en pasos pequeños, probando después de cada cambio
  2. Sobre Repositories: Empezar con CRUD genérico, agregar métodos específicos según necesidad
  3. Sobre Services: Un servicio por entidad principal (UserService, ProjectService, etc.)
  4. Sobre Exceptions: Crear solo las que realmente necesitas, no todas las posibles
  5. Sobre Testing: Esta arquitectura permite testear lógica de negocio sin levantar el servidor

EPISODIO 5: Creación de Modelos y Relaciones

📊 Información del Video

  • Título: FastAPI y NextJS: Creamos los modelos con sus relaciones | Curso App Full Stack #5
  • URL: https://www.youtube.com/watch?v=z1Q8dcP5jWI
  • Duración: 22:06 minutos
  • Vistas: 221
  • Publicado: Hace 2 meses

📝 Resumen Ejecutivo

Este episodio se sumerge en el modelado completo de la base de datos, implementando un esquema relacional robusto con SQLAlchemy. El instructor diseña modelos complejos con diferentes tipos de relaciones (1:1, 1:N, N:M), configura cascadas y comportamientos de eliminación apropiados, e implementa características avanzadas como timestamps automáticos y enums para estados.

Se hace especial énfasis en las mejores prácticas de diseño de bases de datos: normalización, indices, foreign keys con comportamiento explícito, y la separación entre modelos ORM (SQLAlchemy) y esquemas de validación (Pydantic). El episodio culmina con la configuración completa de Alembic para migraciones y la aplicación exitosa del esquema en PostgreSQL.

🎯 Objetivos del Episodio

Objetivo Principal: Diseñar e implementar un esquema completo de base de datos con relaciones complejas

Objetivos Específicos:

  1. ✅ Diseñar esquema ER (Entidad-Relación) completo
  2. ✅ Crear modelo base con timestamps automáticos
  3. ✅ Implementar modelo User completo
  4. ✅ Implementar modelo Project con relación 1:N a User
  5. ✅ Implementar modelo Task con relaciones múltiples
  6. ✅ Implementar modelo Tag con relación N:M a Task
  7. ✅ Configurar Alembic para migraciones
  8. ✅ Crear esquemas Pydantic correspondientes
  9. ✅ Aplicar migraciones en PostgreSQL

🔑 Puntos Clave Técnicos

1. Diseño del Esquema de Base de Datos

Entidades Principales:

  • User: Usuarios del sistema
  • Project: Proyectos creados por usuarios
  • Task: Tareas dentro de proyectos
  • Tag: Etiquetas para categorizar tareas

Relaciones Diseñadas:

User (1) ──< Project (N)     [Un usuario tiene muchos proyectos]
Project (1) ──< Task (N)     [Un proyecto tiene muchas tareas]
User (1) ──< Task (N)        [Un usuario tiene tareas asignadas - opcional]
Task (N) ><── Tag (M)        [Muchas tareas tienen muchos tags]

Decisiones de Diseño:

  • User como "owner" de Projects (no puede ser nulo)
  • Task asignada a User es opcional (puede ser NULL)
  • Cascadas apropiadas para mantener integridad referencial
  • Indices en foreign keys para performance

2. Modelo Base con Timestamps

models/base.py:

from datetime import datetime
from sqlalchemy import Column, Integer, DateTime
from sqlalchemy.ext.declarative import declared_attr
from app.db.base import Base

class TimestampMixin:
    """Mixin para timestamps automáticos"""
    created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
    updated_at = Column(
        DateTime, 
        default=datetime.utcnow, 
        onupdate=datetime.utcnow, 
        nullable=False
    )

class BaseModel(Base, TimestampMixin):
    """Clase base para todos los modelos"""
    __abstract__ = True
    
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    
    @declared_attr
    def __tablename__(cls) -> str:
        return cls.__name__.lower()

Beneficios del Modelo Base:

  • DRY: id, created_at, updated_at en todos los modelos automáticamente
  • datetime.utcnow sin () para lazy evaluation
  • onupdate actualiza updated_at en cada UPDATE
  • __abstract__ = True evita crear tabla para BaseModel
  • declared_attr para generar __tablename__ dinámicamente

3. Modelo User Completo

models/user.py:

from sqlalchemy import Boolean, Column, String
from sqlalchemy.orm import relationship
from app.models.base import BaseModel

class User(BaseModel):
    __tablename__ = "users"
    
    # Campos básicos
    username = Column(String(50), unique=True, index=True, nullable=False)
    email = Column(String(255), unique=True, index=True, nullable=False)
    full_name = Column(String(100), nullable=True)
    hashed_password = Column(String(255), nullable=False)
    
    # Estados booleanos
    is_active = Column(Boolean, default=True)
    is_superuser = Column(Boolean, default=False)
    is_verified = Column(Boolean, default=False)
    
    # Relaciones
    projects = relationship(
        "Project", 
        back_populates="owner",
        cascade="all, delete-orphan",
        lazy="dynamic"
    )
    
    assigned_tasks = relationship(
        "Task",
        back_populates="assignee",
        foreign_keys="[Task.assigned_to_id]",
        lazy="dynamic"
    )

Características Importantes:

  • unique=True en username y email
  • index=True en campos de búsqueda frecuente
  • cascade="all, delete-orphan" elimina proyectos si se elimina usuario
  • lazy="dynamic" para cargar relaciones bajo demanda
  • foreign_keys explícito cuando hay ambigüedad

Esquema Pydantic Correspondiente:

from pydantic import BaseModel, EmailStr, Field

class UserBase(BaseModel):
    email: EmailStr
    username: str = Field(..., min_length=3, max_length=50)
    full_name: Optional[str] = None

class UserCreate(UserBase):
    password: str = Field(..., min_length=8)

class User(UserBase):
    id: int
    is_active: bool
    is_superuser: bool
    created_at: datetime
    updated_at: datetime
    
    class Config:
        from_attributes = True  # Antes orm_mode

4. Relación 1:N (Project pertenece a User)

models/project.py:

from sqlalchemy import Boolean, Column, String, Text, Integer, ForeignKey
from sqlalchemy.orm import relationship
from app.models.base import BaseModel

class Project(BaseModel):
    __tablename__ = "projects"
    
    name = Column(String(100), nullable=False, index=True)
    description = Column(Text, nullable=True)
    is_active = Column(Boolean, default=True)
    
    # Foreign Key a User
    owner_id = Column(
        Integer, 
        ForeignKey("users.id", ondelete="CASCADE"),
        nullable=False,
        index=True
    )
    
    # Relaciones
    owner = relationship("User", back_populates="projects")
    tasks = relationship(
        "Task",
        back_populates="project",
        cascade="all, delete-orphan",
        lazy="dynamic"
    )

Aspectos Clave:

  • ForeignKey("users.id", ondelete="CASCADE")
    • Si se elimina el usuario, se eliminan sus proyectos
    • Integridad referencial a nivel de BD
  • nullable=False en owner_id
    • Proyecto DEBE tener un owner
    • No puede existir proyecto huérfano
  • index=True en foreign keys
    • Acelera queries WHERE owner_id = X
    • Crucial para performance

5. Modelo Task con Relaciones Múltiples y Enums

models/task.py:

from sqlalchemy import Column, String, Text, Integer, ForeignKey, Enum, Date
from sqlalchemy.orm import relationship
import enum
from app.models.base import BaseModel

class TaskStatus(str, enum.Enum):
    TODO = "todo"
    IN_PROGRESS = "in_progress"
    IN_REVIEW = "in_review"
    DONE = "done"
    CANCELLED = "cancelled"

class TaskPriority(str, enum.Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    URGENT = "urgent"

class Task(BaseModel):
    __tablename__ = "tasks"
    
    title = Column(String(200), nullable=False, index=True)
    description = Column(Text, nullable=True)
    status = Column(Enum(TaskStatus), default=TaskStatus.TODO, nullable=False, index=True)
    priority = Column(Enum(TaskPriority), default=TaskPriority.MEDIUM, nullable=False)
    due_date = Column(Date, nullable=True)
    
    # FK a Project (requerido)
    project_id = Column(
        Integer,
        ForeignKey("projects.id", ondelete="CASCADE"),
        nullable=False,
        index=True
    )
    
    # FK a User (opcional)
    assigned_to_id = Column(
        Integer,
        ForeignKey("users.id", ondelete="SET NULL"),
        nullable=True,
        index=True
    )
    
    # Relaciones
    project = relationship("Project", back_populates="tasks")
    assignee = relationship("User", back_populates="assigned_tasks", foreign_keys=[assigned_to_id])
    tags = relationship("Tag", secondary="task_tags", back_populates="tasks", lazy="dynamic")

Características Avanzadas:

  • Enums: Valores restringidos a nivel de BD
    • str, enum.Enum para compatibilidad Pydantic
    • Previene datos inválidos
    • Mejor que strings libres
  • ondelete Diferenciados:
    • CASCADE para project: eliminar task si se elimina proyecto
    • SET NULL para assignee: dejar sin asignar si se elimina usuario
  • Índice en status: Queries frecuentes por estado
  • Relación N:M con tags via tabla intermedia

6. Relación N:M (Task ↔ Tag)

models/tag.py:

from sqlalchemy import Column, String, Integer, ForeignKey, Table
from sqlalchemy.orm import relationship
from app.models.base import BaseModel, Base

# Tabla intermedia (association table)
task_tags = Table(
    'task_tags',
    Base.metadata,
    Column('task_id', Integer, ForeignKey('tasks.id', ondelete="CASCADE"), primary_key=True),
    Column('tag_id', Integer, ForeignKey('tags.id', ondelete="CASCADE"), primary_key=True)
)

class Tag(BaseModel):
    __tablename__ = "tags"
    
    name = Column(String(50), unique=True, nullable=False, index=True)
    color = Column(String(7), default="#808080")  # Hex color
    
    # Relación N:M
    tasks = relationship(
        "Task",
        secondary=task_tags,
        back_populates="tags",
        lazy="dynamic"
    )

Relación N:M Explicada:

  • Tabla Intermedia: task_tags sin modelo ORM
    • Solo une IDs de tasks y tags
    • Primary key compuesta (task_id, tag_id)
    • Cascadas en ambas direcciones
  • secondary: SQLAlchemy maneja automáticamente la tabla intermedia
  • Uso:
    task.tags.append(tag)  # Agregar tag a task
    task.tags.remove(tag)  # Quitar tag de task
    tags_of_task = task.tags.all()  # Obtener tags de task
    tasks_with_tag = tag.tasks.all()  # Obtener tasks con tag
    

7. Migraciones con Alembic

Configuración de Alembic:

# Inicializar Alembic
alembic init alembic

alembic/env.py (configurado):

from app.core.config import settings
from app.db.base import Base
# Importar TODOS los modelos para detección automática
from app.models import user, project, task, tag

config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
target_metadata = Base.metadata

def run_migrations_online():
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )
    with connectable.connect() as connection:
        context.configure(connection=connection, target_metadata=target_metadata)
        with context.begin_transaction():
            context.run_migrations()

Comandos de Alembic:

# Crear migración automática
alembic revision --autogenerate -m "Create all tables"

# Revisar migración generada
# alembic/versions/001_create_all_tables.py

# Aplicar migraciones
alembic upgrade head

# Ver estado actual
alembic current

# Ver historial
alembic history

# Revertir última migración
alembic downgrade -1

# Revertir todo
alembic downgrade base

Migración Generada (simplificado):

def upgrade():
    op.create_table(
        'users',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('username', sa.String(50), nullable=False),
        sa.Column('email', sa.String(255), nullable=False),
        # ... más columnas
        sa.PrimaryKeyConstraint('id'),
        sa.UniqueConstraint('email'),
        sa.UniqueConstraint('username')
    )
    op.create_index('ix_users_email', 'users', ['email'])
    
    op.create_table(
        'projects',
        # ... definición
        sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ondelete='CASCADE')
    )
    
    # ... más tablas

def downgrade():
    op.drop_table('task_tags')
    op.drop_table('tasks')
    op.drop_table('tags')
    op.drop_table('projects')
    op.drop_table('users')

💼 Tecnologías y Conceptos Utilizados

Concepto Implementación Beneficio
SQLAlchemy ORM Modelos declarativos Abstracción de SQL
Alembic Sistema de migraciones Versionado de esquema
Pydantic Schemas Validación de I/O Type safety
Enums TaskStatus, TaskPriority Valores restringidos
Timestamps created_at, updated_at Auditoría automática
Cascadas ondelete behaviors Integridad referencial
Índices index=True en campos clave Performance de queries
Relaciones 1:N User→Projects, Project→Tasks Modelado jerárquico
Relaciones N:M Task↔Tag Clasificación flexible

✅ Resultados Alcanzados

Al finalizar este episodio:

  1. Esquema de BD Completo:

    • 4 tablas principales (users, projects, tasks, tags)
    • 1 tabla intermedia (task_tags)
    • Todas las relaciones configuradas
  2. Modelos SQLAlchemy:

    • Modelo base reutilizable
    • Timestamps automáticos
    • Relaciones bidireccionales
    • Cascadas y comportamientos configurados
  3. Esquemas Pydantic:

    • Schemas de creación (Create)
    • Schemas de actualización (Update)
    • Schemas de respuesta (Response)
    • Validaciones con Field()
  4. Sistema de Migraciones:

    • Alembic configurado
    • Migración inicial creada
    • Esquema aplicado en PostgreSQL
  5. Features Avanzadas:

    • Enums para estados
    • Relaciones opcionales y requeridas
    • Tabla intermedia para N:M
    • Índices para performance
  6. Integridad de Datos:

    • Foreign keys con constraints
    • Unique constraints
    • NOT NULL donde corresponde
    • Cascadas para mantener consistencia

🔗 Conexión con Episodios Previos/Siguientes

Depende de:

  • Episodio 2: Configuración de SQLAlchemy
  • Episodio 4: Repositorios preparados para estos modelos

Utiliza:

  • Arquitectura del Ep. 4: Estos modelos usan los repositorios y servicios

Prepara para:

  • Episodios futuros: CRUD completo con estos modelos
  • Endpoints: API para proyectos, tareas y tags
  • Frontend: UI para gestionar todas estas entidades

Evolución del Proyecto:

Ep 2: Configuración SQLAlchemy básica
Ep 4: Arquitectura en capas
Ep 5: Modelos complejos + Migraciones
      ↓
Próximo: CRUD completo + WebSockets

📌 Notas Importantes

Importar Modelos: En alembic/env.py, importar TODOS los modelos para que Alembic los detecte en autogenerate.

Revisar Migraciones: SIEMPRE revisar migraciones autogeneradas antes de aplicarlas. Alembic puede equivocarse.

Orden de Creación: Las migraciones crean tablas en orden correcto respetando foreign keys (primero users, luego projects, luego tasks).

Cascadas: ondelete="CASCADE" vs ondelete="SET NULL" tienen implicaciones importantes. Elegir según la semántica del negocio.

Enums: SQLAlchemy crea un tipo ENUM en PostgreSQL. Cambiar enums requiere migración cuidadosa.

lazy="dynamic": Retorna query en lugar de lista. Útil para relaciones con muchos registros.

🎬 Estructura del Episodio

[00:00-03:00] → Introducción y diseño del esquema ER
[03:00-07:00] → Implementación del modelo base y User
[07:00-10:00] → Modelo Project con relación 1:N
[10:00-14:00] → Modelo Task con relaciones múltiples y enums
[14:00-17:00] → Modelo Tag y relación N:M
[17:00-20:00] → Configuración de Alembic y creación de migración
[20:00-22:06] → Aplicar migraciones y verificación en PostgreSQL

💡 Tips del Instructor

  1. Sobre Diseño de Esquema: Dibuja el diagrama ER antes de codificar. Ayuda a visualizar relaciones.

  2. Sobre Migraciones: Usa mensajes descriptivos. Serás tú en el futuro quien los lea.

  3. Sobre Enums: Define enums en el modelo SQLAlchemy. Pydantic los importa automáticamente.

  4. Sobre Relaciones: back_populates debe coincidir en ambos modelos o fallará.

  5. Sobre Índices: Indexar foreign keys es crucial. PostgreSQL no lo hace automáticamente.

  6. Sobre Timestamps: Siempre usar UTC (datetime.utcnow). Convertir a timezone local en el frontend.

  7. Sobre Cascadas: Documentar decisiones de ondelete en comentarios. No siempre es obvio por qué se eligió CASCADE vs SET NULL.


📊 RESUMEN COMPARATIVO DE LA SERIE

Duración y Popularidad

Episodio Duración Vistas Me Gusta Complejidad Enfoque Principal
1 10:13 650 55 ★☆☆☆☆ Conceptual
2 43:31 ⭐ 538 33 ★★★☆☆ Implementación Backend
3 28:14 258 18 ★★★☆☆ Implementación Frontend
4 16:14 142 10 ★★★★☆ Arquitectura Avanzada
5 22:06 221 15 ★★★★☆ Modelado de Datos
Total ~2h 1,809 131 - -

Progresión del Conocimiento

Episodio 1: CONCEPTOS
↓ (comprensión general)

Episodio 2: BACKEND BÁSICO
↓ (implementación inicial)

Episodio 3: FRONTEND BÁSICO
↓ (aplicación funcionando end-to-end)

Episodio 4: REFACTORING PROFESIONAL
↓ (código de calidad producción)

Episodio 5: DATOS COMPLEJOS
↓ (modelado avanzado)

[Episodios Futuros]: FEATURES COMPLETAS

Stack Tecnológico Final

┌─────────────────────────────────────────────┐
│           FULL STACK COMPLETO               │
├─────────────────────────────────────────────┤
│ Frontend:                                   │
│  • Next.js 14 (App Router)                 │
│  • TypeScript 5                             │
│  • TailwindCSS 3                            │
│  • shadcn/ui                                │
│  • axios                                    │
│                                             │
│ Backend:                                    │
│  • FastAPI 0.109+                          │
│  • Python 3.11+                             │
│  • SQLAlchemy 2.0                           │
│  • Alembic                                  │
│  • Pydantic 2.5+                            │
│  • JWT (python-jose)                        │
│  • bcrypt (passlib)                         │
│                                             │
│ Base de Datos:                              │
│  • PostgreSQL 15                            │
│                                             │
│ DevOps:                                     │
│  • Docker/Podman                            │
│  • docker-compose                           │
└─────────────────────────────────────────────┘

Conclusión de la Serie (hasta ahora)

Estos 5 episodios establecen una base sólida y profesional para una aplicación Full Stack moderna. La progresión es lógica y bien estructurada:

  1. Visión → Sabes QUÉ vas a construir
  2. Backend → Tienes un servidor funcionando
  3. Frontend → Tienes una UI que consume la API
  4. Arquitectura → Código profesional y escalable
  5. Datos → Modelo de datos robusto

Los próximos episodios (anticipados) cubrirían:

  • CRUD completo para todas las entidades
  • Autenticación completa (JWT, cookies, refresh tokens)
  • WebSockets implementados
  • Testing comprehensivo
  • Despliegue en producción

Valor de la Serie: Este no es un tutorial básico. Es una guía completa para construir aplicaciones profesionales con stack moderno, siguiendo mejores prácticas desde el inicio.


Fin del Documento 3

--------------------------------------------------


DOCUMENTO 4: GUÍA DE IMPLEMENTACIÓN PRÁCTICA

DOCUMENTO 4: GUÍA DE IMPLEMENTACIÓN PRÁCTICA

Aplicación Full Stack con FastAPI y Next.js

Guía Paso a Paso para Replicar el Proyecto


📋 TABLA DE CONTENIDOS

  1. Requisitos Previos y Herramientas
  2. Instalación y Configuración del Entorno
  3. Configuración del Backend (FastAPI)
  4. Configuración del Frontend (Next.js)
  5. Configuración de la Base de Datos
  6. Estructura Completa de Archivos
  7. Código Completo por Módulos
  8. Comandos de Terminal Necesarios
  9. Checklist de Verificación
  10. Troubleshooting Común

1. REQUISITOS PREVIOS Y HERRAMIENTAS

🔧 Software Necesario

Obligatorio

Herramienta Versión Mínima Propósito Descarga
Python 3.11+ Backend con FastAPI https://www.python.org/downloads/
Node.js 18+ LTS Frontend con Next.js https://nodejs.org/
Docker 20+ Contenedorización https://www.docker.com/
Git 2.30+ Control de versiones https://git-scm.com/

Alternativas a Docker

Herramienta Ventaja Descarga
Podman No requiere daemon root https://podman.io/
PostgreSQL Local Sin contenedores https://www.postgresql.org/download/

Editores de Código Recomendados

Editor Ventajas Extensiones Recomendadas
VSCode Ecosistema rico, gratis Python, Pylance, ESLint, Prettier, Tailwind CSS IntelliSense
PyCharm Excelente para Python N/A (integrado)
WebStorm Excelente para Next.js N/A (integrado)

📚 Conocimientos Previos Recomendados

Nivel Mínimo: Intermedio

Backend:

  • ✅ Python básico (funciones, clases, decoradores)
  • ✅ Conceptos de API REST (GET, POST, PUT, DELETE)
  • ✅ SQL básico (SELECT, INSERT, UPDATE, DELETE)
  • ⭐ Async/await en Python (deseable)
  • ⭐ ORM (SQLAlchemy) (deseable)

Frontend:

  • ✅ JavaScript/TypeScript básico
  • ✅ React fundamentals (components, hooks)
  • ✅ CSS básico
  • ⭐ Next.js App Router (deseable)
  • ⭐ TailwindCSS (deseable)

DevOps:

  • ✅ Terminal/Línea de comandos
  • ✅ Conceptos básicos de Docker
  • ⭐ docker-compose (deseable)

🌐 Recursos de Aprendizaje

Si necesitas repasar algún concepto:

  • Python Async: https://docs.python.org/3/library/asyncio.html
  • FastAPI Tutorial: https://fastapi.tiangolo.com/tutorial/
  • Next.js Learn: https://nextjs.org/learn
  • SQLAlchemy Docs: https://docs.sqlalchemy.org/
  • Docker Get Started: https://docs.docker.com/get-started/

2. INSTALACIÓN Y CONFIGURACIÓN DEL ENTORNO

🚀 Paso 1: Verificar Instalaciones

Abre una terminal y verifica que todo esté instalado:

# Verificar Python
python --version
# Debe mostrar: Python 3.11.x o superior

# Verificar pip
pip --version

# Verificar Node.js
node --version
# Debe mostrar: v18.x.x o superior

# Verificar npm
npm --version

# Verificar Docker
docker --version
docker-compose --version

# Verificar Git
git --version

¿Algo falta? Instala desde los links de la sección anterior.

🚀 Paso 2: Crear Estructura del Proyecto

# Crear directorio principal
mkdir fastapi-nextjs-fullstack
cd fastapi-nextjs-fullstack

# Inicializar repositorio Git
git init

# Crear .gitignore
touch .gitignore

Contenido de .gitignore:

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
.env
*.db
*.log

# Node
node_modules/
.next/
out/
.env.local
.env.production.local
.turbo

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Database
*.sql
postgres_data/

# Docker
*.log

🚀 Paso 3: Crear Directorios del Proyecto

# Crear estructura base
mkdir -p backend/app/{api/v1/endpoints,core,db,models,repositories,schemas,services}
mkdir -p frontend/src/{app,components/ui,lib,hooks,types}
mkdir -p postgres_data

# Árbol resultante:
# .
# ├── backend/
# │   └── app/
# ├── frontend/
# │   └── src/
# ├── postgres_data/
# └── .gitignore

3. CONFIGURACIÓN DEL BACKEND (FASTAPI)

📦 Paso 1: Crear Entorno Virtual de Python

cd backend

# Crear entorno virtual
python -m venv venv

# Activar entorno virtual
# En Linux/Mac:
source venv/bin/activate
# En Windows:
venv\Scripts\activate

# Tu terminal ahora debería mostrar (venv) al inicio

⚠️ IMPORTANTE: Siempre activa el entorno virtual antes de trabajar con el backend.

📦 Paso 2: Instalar Dependencias de Python

Crear requirements.txt:

# Framework
fastapi==0.109.0
uvicorn[standard]==0.27.0

# Base de datos
sqlalchemy==2.0.25
alembic==1.13.1
psycopg2-binary==2.9.9

# Validación y configuración
pydantic==2.5.3
pydantic-settings==2.1.0
python-dotenv==1.0.0

# Autenticación
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6

# WebSockets
websockets==12.0

# CORS
python-cors==1.0.0

# Utilidades
python-dateutil==2.8.2

Instalar:

pip install -r requirements.txt

# Verificar instalación
pip list

📦 Paso 3: Configurar Variables de Entorno

Crear backend/.env:

# Aplicación
APP_NAME="FastAPI Full Stack Application"
DEBUG=True
VERSION="1.0.0"

# Base de datos
DATABASE_URL=postgresql://fastapi_user:SecurePassword123@localhost:5432/fullstack_db

# Seguridad
SECRET_KEY=your-super-secret-key-change-this-in-production
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7

# CORS
ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000

# Servidor
HOST=0.0.0.0
PORT=8000

🔐 Generar SECRET_KEY Seguro:

# Usar Python para generar clave aleatoria
python -c "import secrets; print(secrets.token_urlsafe(32))"

# O usar openssl
openssl rand -hex 32

📦 Paso 4: Estructura de Archivos del Backend

Crear todos los archivos Python necesarios:

# Desde backend/

# Archivos de paquete (__init__.py)
touch app/__init__.py
touch app/api/__init__.py
touch app/api/v1/__init__.py
touch app/api/v1/endpoints/__init__.py
touch app/core/__init__.py
touch app/db/__init__.py
touch app/models/__init__.py
touch app/repositories/__init__.py
touch app/schemas/__init__.py
touch app/services/__init__.py

# Archivos principales
touch app/main.py
touch app/core/config.py
touch app/core/security.py
touch app/db/base.py
touch app/db/session.py
touch app/api/deps.py
touch app/api/v1/router.py

# Modelos
touch app/models/base.py
touch app/models/user.py
touch app/models/project.py
touch app/models/task.py
touch app/models/tag.py

# Schemas
touch app/schemas/user.py
touch app/schemas/project.py
touch app/schemas/task.py
touch app/schemas/tag.py
touch app/schemas/token.py

# Repositories
touch app/repositories/base.py
touch app/repositories/user.py
touch app/repositories/project.py

# Services
touch app/services/user_service.py
touch app/services/auth_service.py

# Endpoints
touch app/api/v1/endpoints/auth.py
touch app/api/v1/endpoints/users.py
touch app/api/v1/endpoints/health.py

# Dockerfile
touch Dockerfile

📦 Paso 5: Configurar Alembic

# Desde backend/ con venv activado
alembic init alembic

# Esto crea:
# alembic/
# ├── env.py
# ├── script.py.mako
# └── versions/
# alembic.ini

Modificar alembic.ini:

# Línea ~40: Comentar sqlalchemy.url
# sqlalchemy.url = driver://user:pass@localhost/dbname

# La URL se obtendrá dinámicamente de .env

4. CONFIGURACIÓN DEL FRONTEND (NEXT.JS)

📦 Paso 1: Crear Proyecto Next.js

# Desde la raíz del proyecto
cd ..  # Salir de backend/

# Crear proyecto Next.js
npx create-next-app@latest frontend \
  --typescript \
  --tailwind \
  --app \
  --src-dir \
  --no-git \
  --import-alias "@/*"

# Opciones interactivas (si las pide):
# ✔ Use ESLint? → Yes
# ✔ Use Turbopack? → No
# ✔ Customize default import alias? → No

📦 Paso 2: Instalar Dependencias Adicionales

cd frontend

# Cliente HTTP
npm install axios

# Validación de formularios
npm install react-hook-form zod @hookform/resolvers

# shadcn/ui (componentes UI)
npx shadcn-ui@latest init

# Configuración interactiva de shadcn:
# ✔ Style → Default
# ✔ Base color → Slate
# ✔ CSS variables → Yes

# Instalar componentes shadcn específicos
npx shadcn-ui@latest add button
npx shadcn-ui@latest add input
npx shadcn-ui@latest add card
npx shadcn-ui@latest add form
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add dropdown-menu
npx shadcn-ui@latest add toast
npx shadcn-ui@latest add select
npx shadcn-ui@latest add label

📦 Paso 3: Configurar Variables de Entorno

Crear frontend/.env.local:

# API Backend
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
NEXT_PUBLIC_WS_URL=ws://localhost:8000/ws

# Aplicación
NEXT_PUBLIC_APP_NAME="Full Stack App"

⚠️ Nota: Prefijo NEXT_PUBLIC_ es obligatorio para variables accesibles en el cliente.

📦 Paso 4: Configurar Next.js

Modificar frontend/next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',  // Para Docker
  images: {
    domains: ['localhost'],  // Permitir imágenes de localhost
  },
  eslint: {
    ignoreDuringBuilds: false,
  },
  typescript: {
    ignoreBuildErrors: false,
  },
}

module.exports = nextConfig

📦 Paso 5: Estructura de Archivos del Frontend

# Desde frontend/

# Crear estructura de carpetas adicionales
mkdir -p src/components/layout
mkdir -p src/components/forms
mkdir -p src/lib
mkdir -p src/hooks
mkdir -p src/types
mkdir -p src/app/(auth)/login
mkdir -p src/app/(auth)/register
mkdir -p src/app/(protected)/dashboard

# Crear archivos principales
touch src/lib/api.ts
touch src/lib/auth.ts
touch src/lib/utils.ts
touch src/hooks/useAuth.ts
touch src/types/api.ts
touch src/components/layout/Header.tsx
touch src/components/layout/Footer.tsx

5. CONFIGURACIÓN DE LA BASE DE DATOS

🐘 Opción A: PostgreSQL con Docker (Recomendado)

Crear docker-compose.yml en la raíz del proyecto:

version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    container_name: fullstack_postgres
    environment:
      POSTGRES_USER: fastapi_user
      POSTGRES_PASSWORD: SecurePassword123
      POSTGRES_DB: fullstack_db
    ports:
      - "5432:5432"
    volumes:
      - ./postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U fastapi_user -d fullstack_db"]
      interval: 10s
      timeout: 5s
      retries: 5

  backend:
    build: ./backend
    container_name: fullstack_backend
    environment:
      DATABASE_URL: postgresql://fastapi_user:SecurePassword123@postgres:5432/fullstack_db
    env_file:
      - ./backend/.env
    ports:
      - "8000:8000"
    volumes:
      - ./backend/app:/app/app
    depends_on:
      postgres:
        condition: service_healthy
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

  frontend:
    build: ./frontend
    container_name: fullstack_frontend
    environment:
      NEXT_PUBLIC_API_URL: http://localhost:8000/api/v1
    ports:
      - "3000:3000"
    volumes:
      - ./frontend/src:/app/src
    depends_on:
      - backend
    command: npm run dev

volumes:
  postgres_data:

Iniciar servicios:

# Desde la raíz del proyecto
docker-compose up -d postgres

# Verificar que PostgreSQL esté corriendo
docker-compose ps

# Ver logs
docker-compose logs -f postgres

🐘 Opción B: PostgreSQL Local (Sin Docker)

Instalación:

# Ubuntu/Debian
sudo apt update
sudo apt install postgresql postgresql-contrib

# macOS (con Homebrew)
brew install postgresql@15
brew services start postgresql@15

# Windows: Descargar desde postgresql.org

Crear base de datos:

# Conectarse a PostgreSQL
psql postgres

# Dentro de psql:
CREATE USER fastapi_user WITH PASSWORD 'SecurePassword123';
CREATE DATABASE fullstack_db OWNER fastapi_user;
GRANT ALL PRIVILEGES ON DATABASE fullstack_db TO fastapi_user;
\q

Actualizar backend/.env:

DATABASE_URL=postgresql://fastapi_user:SecurePassword123@localhost:5432/fullstack_db

6. ESTRUCTURA COMPLETA DE ARCHIVOS

📁 Vista Completa del Proyecto

fastapi-nextjs-fullstack/
├── backend/
│   ├── alembic/                    # Migraciones de BD
│   │   ├── versions/
│   │   ├── env.py
│   │   └── script.py.mako
│   ├── app/
│   │   ├── __init__.py
│   │   ├── main.py                 # ⭐ Punto de entrada
│   │   ├── api/
│   │   │   ├── __init__.py
│   │   │   ├── deps.py             # ⭐ Dependencias
│   │   │   └── v1/
│   │   │       ├── __init__.py
│   │   │       ├── router.py       # ⭐ Router principal v1
│   │   │       └── endpoints/
│   │   │           ├── __init__.py
│   │   │           ├── auth.py     # Login, register
│   │   │           ├── users.py    # CRUD usuarios
│   │   │           └── health.py   # Health check
│   │   ├── core/
│   │   │   ├── __init__.py
│   │   │   ├── config.py           # ⭐ Configuración
│   │   │   └── security.py         # ⭐ JWT, hashing
│   │   ├── db/
│   │   │   ├── __init__.py
│   │   │   ├── base.py             # Base declarativa
│   │   │   └── session.py          # ⭐ SessionLocal
│   │   ├── models/
│   │   │   ├── __init__.py
│   │   │   ├── base.py             # ⭐ BaseModel
│   │   │   ├── user.py             # ⭐ Modelo User
│   │   │   ├── project.py          # Modelo Project
│   │   │   ├── task.py             # Modelo Task
│   │   │   └── tag.py              # Modelo Tag
│   │   ├── repositories/
│   │   │   ├── __init__.py
│   │   │   ├── base.py             # ⭐ BaseRepository
│   │   │   ├── user.py             # UserRepository
│   │   │   └── project.py          # ProjectRepository
│   │   ├── schemas/
│   │   │   ├── __init__.py
│   │   │   ├── user.py             # ⭐ UserCreate, UserResponse
│   │   │   ├── project.py
│   │   │   ├── task.py
│   │   │   ├── tag.py
│   │   │   └── token.py            # ⭐ Token schemas
│   │   └── services/
│   │       ├── __init__.py
│   │       ├── user_service.py     # ⭐ Lógica de negocio User
│   │       └── auth_service.py     # ⭐ Lógica de autenticación
│   ├── tests/                      # Tests (futuro)
│   ├── .env                        # ⚠️ NO commitear
│   ├── .env.example                # Template de .env
│   ├── requirements.txt            # ⭐ Dependencias Python
│   ├── Dockerfile                  # ⭐ Imagen Docker backend
│   ├── alembic.ini                 # Config Alembic
│   └── README.md
│
├── frontend/
│   ├── public/
│   ├── src/
│   │   ├── app/
│   │   │   ├── layout.tsx          # ⭐ Root layout
│   │   │   ├── page.tsx            # ⭐ Home page
│   │   │   ├── (auth)/             # Grupo de rutas
│   │   │   │   ├── login/
│   │   │   │   │   └── page.tsx
│   │   │   │   └── register/
│   │   │   │       └── page.tsx
│   │   │   └── (protected)/        # Rutas protegidas
│   │   │       └── dashboard/
│   │   │           └── page.tsx
│   │   ├── components/
│   │   │   ├── ui/                 # ⭐ shadcn components
│   │   │   │   ├── button.tsx
│   │   │   │   ├── input.tsx
│   │   │   │   ├── card.tsx
│   │   │   │   └── ...
│   │   │   ├── layout/
│   │   │   │   ├── Header.tsx
│   │   │   │   └── Footer.tsx
│   │   │   └── forms/
│   │   ├── lib/
│   │   │   ├── api.ts              # ⭐ Cliente API axios
│   │   │   ├── auth.ts             # Helpers de auth
│   │   │   └── utils.ts            # Utilidades
│   │   ├── hooks/
│   │   │   ├── useAuth.ts          # ⭐ Hook de autenticación
│   │   │   └── useWebSocket.ts
│   │   ├── types/
│   │   │   └── api.ts              # ⭐ Tipos TypeScript
│   │   └── styles/
│   │       └── globals.css         # ⭐ Estilos globales
│   ├── .env.local                  # ⚠️ NO commitear
│   ├── .env.example
│   ├── next.config.js              # ⭐ Config Next.js
│   ├── tailwind.config.ts          # ⭐ Config Tailwind
│   ├── tsconfig.json               # ⭐ Config TypeScript
│   ├── package.json                # ⭐ Dependencias Node
│   ├── Dockerfile                  # ⭐ Imagen Docker frontend
│   └── README.md
│
├── postgres_data/                  # ⚠️ NO commitear (volumen Docker)
├── docker-compose.yml              # ⭐ Orquestación de servicios
├── .gitignore                      # ⭐ Archivos ignorados por Git
└── README.md                       # ⭐ Documentación principal

Leyenda:

  • ⭐ Archivo crítico/principal
  • ⚠️ No debe estar en Git
  • 📁 Carpeta

7. CÓDIGO COMPLETO POR MÓDULOS

🐍 BACKEND - Código Python

7.1. Configuración Principal

backend/app/core/config.py:

from pydantic_settings import BaseSettings
from typing import List

class Settings(BaseSettings):
    # Aplicación
    APP_NAME: str = "FastAPI Full Stack"
    DEBUG: bool = False
    VERSION: str = "1.0.0"
    
    # Base de datos
    DATABASE_URL: str
    
    # Seguridad
    SECRET_KEY: str
    ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
    REFRESH_TOKEN_EXPIRE_DAYS: int = 7
    
    # CORS
    ALLOWED_ORIGINS: List[str] = ["http://localhost:3000"]
    
    # Servidor
    HOST: str = "0.0.0.0"
    PORT: int = 8000
    
    class Config:
        env_file = ".env"
        case_sensitive = True

settings = Settings()

7.2. Seguridad y Autenticación

backend/app/core/security.py:

from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.core.config import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """Verificar contraseña"""
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    """Hashear contraseña"""
    return pwd_context.hash(password)

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """Crear JWT access token"""
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(
            minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
        )
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
    return encoded_jwt

def decode_token(token: str) -> Optional[dict]:
    """Decodificar JWT token"""
    try:
        payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
        return payload
    except JWTError:
        return None

7.3. Base de Datos

backend/app/db/base.py:

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

backend/app/db/session.py:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.core.config import settings

engine = create_engine(
    settings.DATABASE_URL,
    pool_pre_ping=True,
    pool_size=10,
    max_overflow=20
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def get_db():
    """Dependency para obtener sesión de BD"""
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

7.4. Modelos SQLAlchemy

backend/app/models/base.py:

from datetime import datetime
from sqlalchemy import Column, Integer, DateTime
from sqlalchemy.ext.declarative import declared_attr
from app.db.base import Base

class TimestampMixin:
    """Mixin para timestamps automáticos"""
    created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
    updated_at = Column(
        DateTime, 
        default=datetime.utcnow, 
        onupdate=datetime.utcnow, 
        nullable=False
    )

class BaseModel(Base, TimestampMixin):
    """Clase base para todos los modelos"""
    __abstract__ = True
    
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    
    @declared_attr
    def __tablename__(cls) -> str:
        return cls.__name__.lower()

backend/app/models/user.py:

from sqlalchemy import Boolean, Column, String
from sqlalchemy.orm import relationship
from app.models.base import BaseModel

class User(BaseModel):
    __tablename__ = "users"
    
    username = Column(String(50), unique=True, index=True, nullable=False)
    email = Column(String(255), unique=True, index=True, nullable=False)
    full_name = Column(String(100), nullable=True)
    hashed_password = Column(String(255), nullable=False)
    
    is_active = Column(Boolean, default=True)
    is_superuser = Column(Boolean, default=False)
    is_verified = Column(Boolean, default=False)
    
    # Relaciones
    projects = relationship(
        "Project", 
        back_populates="owner",
        cascade="all, delete-orphan",
        lazy="dynamic"
    )
    
    assigned_tasks = relationship(
        "Task",
        back_populates="assignee",
        foreign_keys="[Task.assigned_to_id]",
        lazy="dynamic"
    )

backend/app/models/project.py:

from sqlalchemy import Boolean, Column, String, Text, Integer, ForeignKey
from sqlalchemy.orm import relationship
from app.models.base import BaseModel

class Project(BaseModel):
    __tablename__ = "projects"
    
    name = Column(String(100), nullable=False, index=True)
    description = Column(Text, nullable=True)
    is_active = Column(Boolean, default=True)
    
    owner_id = Column(
        Integer, 
        ForeignKey("users.id", ondelete="CASCADE"),
        nullable=False,
        index=True
    )
    
    # Relaciones
    owner = relationship("User", back_populates="projects")
    tasks = relationship(
        "Task",
        back_populates="project",
        cascade="all, delete-orphan",
        lazy="dynamic"
    )

backend/app/models/task.py:

from sqlalchemy import Column, String, Text, Integer, ForeignKey, Enum, Date
from sqlalchemy.orm import relationship
import enum
from app.models.base import BaseModel

class TaskStatus(str, enum.Enum):
    TODO = "todo"
    IN_PROGRESS = "in_progress"
    IN_REVIEW = "in_review"
    DONE = "done"
    CANCELLED = "cancelled"

class TaskPriority(str, enum.Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    URGENT = "urgent"

class Task(BaseModel):
    __tablename__ = "tasks"
    
    title = Column(String(200), nullable=False, index=True)
    description = Column(Text, nullable=True)
    status = Column(Enum(TaskStatus), default=TaskStatus.TODO, nullable=False, index=True)
    priority = Column(Enum(TaskPriority), default=TaskPriority.MEDIUM, nullable=False)
    due_date = Column(Date, nullable=True)
    
    project_id = Column(
        Integer,
        ForeignKey("projects.id", ondelete="CASCADE"),
        nullable=False,
        index=True
    )
    
    assigned_to_id = Column(
        Integer,
        ForeignKey("users.id", ondelete="SET NULL"),
        nullable=True,
        index=True
    )
    
    # Relaciones
    project = relationship("Project", back_populates="tasks")
    assignee = relationship("User", back_populates="assigned_tasks", foreign_keys=[assigned_to_id])
    tags = relationship("Tag", secondary="task_tags", back_populates="tasks", lazy="dynamic")

backend/app/models/tag.py:

from sqlalchemy import Column, String, Integer, ForeignKey, Table
from sqlalchemy.orm import relationship
from app.models.base import BaseModel, Base

task_tags = Table(
    'task_tags',
    Base.metadata,
    Column('task_id', Integer, ForeignKey('tasks.id', ondelete="CASCADE"), primary_key=True),
    Column('tag_id', Integer, ForeignKey('tags.id', ondelete="CASCADE"), primary_key=True)
)

class Tag(BaseModel):
    __tablename__ = "tags"
    
    name = Column(String(50), unique=True, nullable=False, index=True)
    color = Column(String(7), default="#808080")
    
    tasks = relationship(
        "Task",
        secondary=task_tags,
        back_populates="tags",
        lazy="dynamic"
    )

7.5. Schemas Pydantic

backend/app/schemas/user.py:

from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime

class UserBase(BaseModel):
    email: EmailStr
    username: str = Field(..., min_length=3, max_length=50)
    full_name: Optional[str] = None

class UserCreate(UserBase):
    password: str = Field(..., min_length=8)

class UserUpdate(BaseModel):
    email: Optional[EmailStr] = None
    full_name: Optional[str] = None
    password: Optional[str] = None

class UserResponse(UserBase):
    id: int
    is_active: bool
    is_superuser: bool
    is_verified: bool
    created_at: datetime
    updated_at: datetime
    
    class Config:
        from_attributes = True

class UserInDB(UserResponse):
    hashed_password: str

backend/app/schemas/token.py:

from pydantic import BaseModel
from typing import Optional

class Token(BaseModel):
    access_token: str
    token_type: str = "bearer"

class TokenPayload(BaseModel):
    sub: Optional[int] = None

7.6. Repositories

backend/app/repositories/base.py:

from typing import TypeVar, Generic, Type, Optional, List
from sqlalchemy.orm import Session
from app.models.base import BaseModel

ModelType = TypeVar("ModelType", bound=BaseModel)

class BaseRepository(Generic[ModelType]):
    def __init__(self, model: Type[ModelType]):
        self.model = model
    
    def get(self, db: Session, id: int) -> Optional[ModelType]:
        return db.query(self.model).filter(self.model.id == id).first()
    
    def get_multi(
        self, db: Session, *, skip: int = 0, limit: int = 100
    ) -> List[ModelType]:
        return db.query(self.model).offset(skip).limit(limit).all()
    
    def create(self, db: Session, *, obj_in: dict) -> ModelType:
        db_obj = self.model(**obj_in)
        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj
    
    def update(
        self, db: Session, *, db_obj: ModelType, obj_in: dict
    ) -> ModelType:
        for field, value in obj_in.items():
            setattr(db_obj, field, value)
        db.add(db_obj)
        db.commit()
        db.refresh(db_obj)
        return db_obj
    
    def delete(self, db: Session, *, id: int) -> Optional[ModelType]:
        obj = db.query(self.model).get(id)
        if obj:
            db.delete(obj)
            db.commit()
        return obj

backend/app/repositories/user.py:

from typing import Optional
from sqlalchemy.orm import Session
from app.models.user import User
from app.repositories.base import BaseRepository

class UserRepository(BaseRepository[User]):
    def __init__(self):
        super().__init__(User)
    
    def get_by_email(self, db: Session, *, email: str) -> Optional[User]:
        return db.query(User).filter(User.email == email).first()
    
    def get_by_username(self, db: Session, *, username: str) -> Optional[User]:
        return db.query(User).filter(User.username == username).first()

user_repository = UserRepository()

7.7. Services

backend/app/services/auth_service.py:

from typing import Optional
from sqlalchemy.orm import Session
from fastapi import HTTPException, status
from app.schemas.user import UserCreate, UserResponse
from app.schemas.token import Token
from app.repositories.user import user_repository
from app.core.security import (
    verify_password,
    get_password_hash,
    create_access_token
)
from datetime import timedelta
from app.core.config import settings

class AuthService:
    def register_user(self, db: Session, user_in: UserCreate) -> UserResponse:
        """Registrar nuevo usuario"""
        # Verificar email único
        if user_repository.get_by_email(db, email=user_in.email):
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT,
                detail="Email ya registrado"
            )
        
        # Verificar username único
        if user_repository.get_by_username(db, username=user_in.username):
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT,
                detail="Username ya existe"
            )
        
        # Hash de contraseña
        hashed_password = get_password_hash(user_in.password)
        
        # Crear usuario
        user_data = user_in.dict(exclude={'password'})
        user_data['hashed_password'] = hashed_password
        
        user = user_repository.create(db, obj_in=user_data)
        return UserResponse.from_orm(user)
    
    def authenticate(
        self, db: Session, *, email: str, password: str
    ) -> Optional[Token]:
        """Autenticar usuario y retornar token"""
        user = user_repository.get_by_email(db, email=email)
        
        if not user:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Credenciales inválidas"
            )
        
        if not verify_password(password, user.hashed_password):
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Credenciales inválidas"
            )
        
        if not user.is_active:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Usuario inactivo"
            )
        
        # Crear token
        access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
        access_token = create_access_token(
            data={"sub": str(user.id)},
            expires_delta=access_token_expires
        )
        
        return Token(access_token=access_token, token_type="bearer")

auth_service = AuthService()

7.8. Dependencias

backend/app/api/deps.py:

from typing import Generator, Optional
from fastapi import Depends, HTTPException, status, Cookie
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.models.user import User
from app.repositories.user import user_repository
from app.core.security import decode_token

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login", auto_error=False)

async def get_current_user(
    db: Session = Depends(get_db),
    token: Optional[str] = Depends(oauth2_scheme),
    access_token: Optional[str] = Cookie(None)
) -> User:
    """Obtener usuario actual desde JWT"""
    # Priorizar cookie sobre header
    token_to_decode = access_token or token
    
    if not token_to_decode:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="No autenticado"
        )
    
    payload = decode_token(token_to_decode)
    if not payload:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Token inválido"
        )
    
    user_id = int(payload.get("sub"))
    user = user_repository.get(db, id=user_id)
    
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Usuario no encontrado"
        )
    
    return user

async def get_current_active_user(
    current_user: User = Depends(get_current_user),
) -> User:
    """Verificar usuario activo"""
    if not current_user.is_active:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Usuario inactivo"
        )
    return current_user

7.9. Endpoints

backend/app/api/v1/endpoints/auth.py:

from fastapi import APIRouter, Depends, status, Response
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.schemas.user import UserCreate, UserResponse
from app.schemas.token import Token
from app.services.auth_service import auth_service
from app.api.deps import get_db

router = APIRouter()

@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
def register(
    user_in: UserCreate,
    db: Session = Depends(get_db)
):
    """Registrar nuevo usuario"""
    return auth_service.register_user(db, user_in=user_in)

@router.post("/login", response_model=Token)
def login(
    response: Response,
    form_data: OAuth2PasswordRequestForm = Depends(),
    db: Session = Depends(get_db)
):
    """Login y obtener token"""
    token = auth_service.authenticate(
        db,
        email=form_data.username,  # OAuth2 usa 'username', nosotros lo tratamos como email
        password=form_data.password
    )
    
    # Establecer token en cookie HttpOnly
    response.set_cookie(
        key="access_token",
        value=token.access_token,
        httponly=True,
        secure=False,  # True en producción con HTTPS
        samesite="lax",
        max_age=1800  # 30 minutos
    )
    
    return token

@router.post("/logout")
def logout(response: Response):
    """Logout (eliminar cookie)"""
    response.delete_cookie(key="access_token")
    return {"message": "Logout exitoso"}

backend/app/api/v1/endpoints/users.py:

from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.schemas.user import UserResponse
from app.api.deps import get_db, get_current_active_user
from app.models.user import User

router = APIRouter()

@router.get("/me", response_model=UserResponse)
def get_current_user_info(
    current_user: User = Depends(get_current_active_user)
):
    """Obtener información del usuario actual"""
    return current_user

backend/app/api/v1/endpoints/health.py:

from fastapi import APIRouter
from app.core.config import settings

router = APIRouter()

@router.get("/health")
def health_check():
    """Health check endpoint"""
    return {
        "status": "healthy",
        "app_name": settings.APP_NAME,
        "version": settings.VERSION
    }

7.10. Router y Main

backend/app/api/v1/router.py:

from fastapi import APIRouter
from app.api.v1.endpoints import auth, users, health

api_router = APIRouter()

api_router.include_router(health.router, tags=["health"])
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
api_router.include_router(users.router, prefix="/users", tags=["users"])

backend/app/main.py:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import settings
from app.api.v1.router import api_router

app = FastAPI(
    title=settings.APP_NAME,
    version=settings.VERSION,
    docs_url="/docs",
    redoc_url="/redoc"
)

# CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.ALLOWED_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Incluir router versionado
app.include_router(api_router, prefix="/api/v1")

@app.get("/")
def root():
    return {
        "message": "FastAPI Full Stack API",
        "docs": "/docs",
        "version": settings.VERSION
    }

7.11. Docker

backend/Dockerfile:

FROM python:3.11-slim

WORKDIR /app

# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \
    gcc \
    postgresql-client \
    && rm -rf /var/lib/apt/lists/*

# Copiar requirements e instalar
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir -r requirements.txt

# Copiar código
COPY ./app ./app
COPY alembic.ini .
COPY ./alembic ./alembic

EXPOSE 8000

# Comando por defecto (puede sobrescribirse en docker-compose)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

7.12. Alembic

alembic/env.py (modificado):

from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context

# Importar settings y Base
from app.core.config import settings
from app.db.base import Base

# Importar TODOS los modelos
from app.models import user, project, task, tag

# Configurar URL de BD
config = context.config
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)

if config.config_file_name is not None:
    fileConfig(config.config_file_name)

target_metadata = Base.metadata

def run_migrations_offline():
    url = config.get_main_option("sqlalchemy.url")
    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        dialect_opts={"paramstyle": "named"},
    )

    with context.begin_transaction():
        context.run_migrations()

def run_migrations_online():
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )

    with connectable.connect() as connection:
        context.configure(
            connection=connection, target_metadata=target_metadata
        )

        with context.begin_transaction():
            context.run_migrations()

if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()

⚛️ FRONTEND - Código TypeScript/React

7.13. Cliente API

frontend/src/lib/api.ts:

import axios, { AxiosInstance, AxiosError } from 'axios';

const apiClient: AxiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  timeout: 10000,
  withCredentials: true,  // Importante para cookies
  headers: {
    'Content-Type': 'application/json',
  },
});

// Interceptor para manejo de errores
apiClient.interceptors.response.use(
  response => response,
  (error: AxiosError) => {
    if (error.response?.status === 401) {
      // Redirigir a login
      if (typeof window !== 'undefined') {
        window.location.href = '/login';
      }
    }
    return Promise.reject(error);
  }
);

// API Methods
export const api = {
  auth: {
    register: async (data: { email: string; username: string; password: string }) => {
      return apiClient.post('/auth/register', data);
    },
    login: async (email: string, password: string) => {
      const formData = new FormData();
      formData.append('username', email);  // OAuth2 expects 'username'
      formData.append('password', password);
      return apiClient.post('/auth/login', formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
      });
    },
    logout: async () => {
      return apiClient.post('/auth/logout');
    },
    getCurrentUser: async () => {
      return apiClient.get('/users/me');
    },
  },
  // Agregar más endpoints según necesidad
};

export default apiClient;

7.14. Tipos TypeScript

frontend/src/types/api.ts:

export interface User {
  id: number;
  email: string;
  username: string;
  full_name?: string;
  is_active: boolean;
  is_superuser: boolean;
  is_verified: boolean;
  created_at: string;
  updated_at: string;
}

export interface LoginCredentials {
  email: string;
  password: string;
}

export interface RegisterData {
  email: string;
  username: string;
  password: string;
  full_name?: string;
}

export interface Token {
  access_token: string;
  token_type: string;
}

7.15. Hook de Autenticación

frontend/src/hooks/useAuth.ts:

'use client';

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { api } from '@/lib/api';
import { User, LoginCredentials, RegisterData } from '@/types/api';

export function useAuth() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const router = useRouter();

  useEffect(() => {
    checkAuth();
  }, []);

  const checkAuth = async () => {
    try {
      const response = await api.auth.getCurrentUser();
      setUser(response.data);
    } catch (error) {
      setUser(null);
    } finally {
      setLoading(false);
    }
  };

  const login = async (credentials: LoginCredentials) => {
    try {
      await api.auth.login(credentials.email, credentials.password);
      await checkAuth();
      router.push('/dashboard');
    } catch (error) {
      throw error;
    }
  };

  const register = async (data: RegisterData) => {
    try {
      await api.auth.register(data);
      // Auto-login después de registro
      await login({ email: data.email, password: data.password });
    } catch (error) {
      throw error;
    }
  };

  const logout = async () => {
    try {
      await api.auth.logout();
      setUser(null);
      router.push('/login');
    } catch (error) {
      console.error('Logout error:', error);
    }
  };

  return {
    user,
    loading,
    login,
    register,
    logout,
    checkAuth,
  };
}

7.16. Layouts

frontend/src/app/layout.tsx:

import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Full Stack App',
  description: 'FastAPI + Next.js Full Stack Application',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="es">
      <body className={inter.className}>
        {children}
      </body>
    </html>
  )
}

7.17. Página de Login

frontend/src/app/(auth)/login/page.tsx:

'use client';

import { useState } from 'react';
import { useAuth } from '@/hooks/useAuth';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';

export default function LoginPage() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const { login } = useAuth();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');
    
    try {
      await login({ email, password });
    } catch (err: any) {
      setError(err.response?.data?.detail || 'Error al iniciar sesión');
    }
  };

  return (
    <div className="flex min-h-screen items-center justify-center">
      <Card className="w-full max-w-md">
        <CardHeader>
          <CardTitle>Iniciar Sesión</CardTitle>
        </CardHeader>
        <CardContent>
          <form onSubmit={handleSubmit} className="space-y-4">
            {error && (
              <div className="bg-red-50 text-red-600 p-3 rounded-md text-sm">
                {error}
              </div>
            )}
            
            <div>
              <Input
                type="email"
                placeholder="Email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                required
              />
            </div>
            
            <div>
              <Input
                type="password"
                placeholder="Contraseña"
                value={password}
                onChange={(e) => setPassword(e.target.value)}
                required
              />
            </div>
            
            <Button type="submit" className="w-full">
              Iniciar Sesión
            </Button>
          </form>
        </CardContent>
      </Card>
    </div>
  );
}

7.18. Frontend Dockerfile

frontend/Dockerfile:

FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
ENV PORT 3000

CMD ["node", "server.js"]

8. COMANDOS DE TERMINAL NECESARIOS

🔧 Comandos de Configuración Inicial

# 1. Clonar o crear proyecto
git init fastapi-nextjs-fullstack
cd fastapi-nextjs-fullstack

# 2. Backend: Crear entorno virtual
cd backend
python -m venv venv
source venv/bin/activate  # Linux/Mac
# venv\Scripts\activate   # Windows

# 3. Backend: Instalar dependencias
pip install -r requirements.txt

# 4. Backend: Configurar Alembic
alembic init alembic

# 5. Frontend: Crear proyecto Next.js
cd ..
npx create-next-app@latest frontend --typescript --tailwind --app --src-dir

# 6. Frontend: Instalar dependencias adicionales
cd frontend
npm install axios react-hook-form zod @hookform/resolvers
npx shadcn-ui@latest init
npx shadcn-ui@latest add button input card form dialog

🐘 Comandos de Base de Datos

# Con Docker
docker-compose up -d postgres
docker-compose logs -f postgres

# Conectarse a PostgreSQL en Docker
docker exec -it fullstack_postgres psql -U fastapi_user -d fullstack_db

# Sin Docker (local)
psql postgres
CREATE USER fastapi_user WITH PASSWORD 'SecurePassword123';
CREATE DATABASE fullstack_db OWNER fastapi_user;
\q

# Verificar conexión
psql -U fastapi_user -d fullstack_db -h localhost

# Dentro de psql:
\dt          # Listar tablas
\d users     # Describir tabla users
\q           # Salir

🔄 Comandos de Migraciones (Alembic)

# Desde backend/ con venv activado

# Crear migración automática
alembic revision --autogenerate -m "Mensaje descriptivo"

# Ejemplos:
alembic revision --autogenerate -m "Create initial tables"
alembic revision --autogenerate -m "Add is_verified to users"

# Aplicar migraciones
alembic upgrade head

# Ver estado actual
alembic current

# Ver historial
alembic history

# Revertir última migración
alembic downgrade -1

# Revertir a revisión específica
alembic downgrade <revision_id>

# Revertir todo
alembic downgrade base

🚀 Comandos para Iniciar Servicios

Opción A: Con Docker Compose (Recomendado)

# Desde la raíz del proyecto

# Iniciar todos los servicios
docker-compose up

# Iniciar en segundo plano
docker-compose up -d

# Ver logs
docker-compose logs -f

# Ver logs de un servicio específico
docker-compose logs -f backend
docker-compose logs -f frontend
docker-compose logs -f postgres

# Detener servicios
docker-compose down

# Detener y eliminar volúmenes (⚠️ ELIMINA DATOS)
docker-compose down -v

# Reconstruir imágenes
docker-compose build

# Reconstruir e iniciar
docker-compose up --build

Opción B: Sin Docker (Local)

Terminal 1 - Backend:

cd backend
source venv/bin/activate
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

# El backend estará en: http://localhost:8000
# Docs en: http://localhost:8000/docs

Terminal 2 - Frontend:

cd frontend
npm run dev

# El frontend estará en: http://localhost:3000

Terminal 3 - PostgreSQL (si no está como servicio):

# macOS con Homebrew
brew services start postgresql@15

# Linux (Debian/Ubuntu)
sudo service postgresql start

🧪 Comandos de Testing y Desarrollo

# Backend: Formatear código con Black
cd backend
pip install black
black app/

# Backend: Linting con flake8
pip install flake8
flake8 app/

# Backend: Type checking con mypy
pip install mypy
mypy app/

# Frontend: Linting
cd frontend
npm run lint

# Frontend: Formatear con Prettier
npm run format  # (si está configurado)

# Frontend: Build de producción
npm run build

# Frontend: Iniciar build de producción
npm start

📊 Comandos de Docker Útiles

# Ver contenedores corriendo
docker ps

# Ver todos los contenedores
docker ps -a

# Ver logs de contenedor
docker logs fullstack_backend
docker logs -f fullstack_backend  # Seguir logs

# Entrar a contenedor
docker exec -it fullstack_backend /bin/bash
docker exec -it fullstack_postgres /bin/bash

# Ver imágenes
docker images

# Eliminar contenedor
docker rm fullstack_backend

# Eliminar imagen
docker rmi <image_id>

# Limpiar recursos no utilizados
docker system prune

# Ver uso de espacio
docker system df

# Reiniciar contenedor
docker restart fullstack_backend

🔍 Comandos de Debugging

# Backend: Verificar configuración
cd backend
source venv/bin/activate
python -c "from app.core.config import settings; print(settings.DATABASE_URL)"

# Backend: Probar conexión a BD
python -c "from app.db.session import engine; engine.connect(); print('✓ Conexión exitosa')"

# Backend: Shell interactivo
python
>>> from app.db.session import SessionLocal
>>> from app.models.user import User
>>> db = SessionLocal()
>>> users = db.query(User).all()
>>> print(users)

# PostgreSQL: Verificar datos
docker exec -it fullstack_postgres psql -U fastapi_user -d fullstack_db -c "SELECT * FROM users;"

# Frontend: Verificar variables de entorno
cd frontend
npm run build
# Las variables NEXT_PUBLIC_* se mostrarán si hay errores

9. CHECKLIST DE VERIFICACIÓN

✅ Fase 1: Instalación de Herramientas

  • [ ] Python 3.11+ instalado (python --version)
  • [ ] pip actualizado (pip install --upgrade pip)
  • [ ] Node.js 18+ instalado (node --version)
  • [ ] npm actualizado (npm install -g npm@latest)
  • [ ] Docker instalado y corriendo (docker --version, docker ps)
  • [ ] docker-compose instalado (docker-compose --version)
  • [ ] Git instalado (git --version)
  • [ ] Editor de código configurado (VSCode con extensiones recomendadas)

✅ Fase 2: Configuración del Proyecto

  • [ ] Repositorio Git inicializado
  • [ ] .gitignore creado con contenido apropiado
  • [ ] Estructura de carpetas creada (backend/, frontend/)
  • [ ] backend/.env configurado con variables correctas
  • [ ] frontend/.env.local configurado
  • [ ] docker-compose.yml creado en raíz

✅ Fase 3: Backend (FastAPI)

  • [ ] Entorno virtual creado y activado
  • [ ] Dependencias instaladas (pip list muestra paquetes)
  • [ ] requirements.txt creado con todas las dependencias
  • [ ] Estructura de carpetas backend creada correctamente
  • [ ] Archivos __init__.py en todos los paquetes
  • [ ] app/main.py implementado
  • [ ] app/core/config.py con Settings configurado
  • [ ] app/core/security.py con funciones JWT
  • [ ] app/db/session.py con engine y SessionLocal
  • [ ] Modelos SQLAlchemy creados (user.py, project.py, etc.)
  • [ ] Schemas Pydantic creados
  • [ ] Repositorios implementados (BaseRepository, UserRepository)
  • [ ] Servicios implementados (AuthService, UserService)
  • [ ] Endpoints creados (auth.py, users.py, health.py)
  • [ ] Router v1 configurado
  • [ ] CORS middleware configurado
  • [ ] Alembic inicializado
  • [ ] alembic/env.py modificado correctamente
  • [ ] Dockerfile backend creado

✅ Fase 4: Base de Datos

  • [ ] PostgreSQL corriendo (Docker o local)
  • [ ] Base de datos fullstack_db creada
  • [ ] Usuario fastapi_user creado con permisos
  • [ ] Conexión desde backend exitosa
  • [ ] Primera migración creada (alembic revision --autogenerate)
  • [ ] Migraciones aplicadas (alembic upgrade head)
  • [ ] Tablas creadas en PostgreSQL (verificar con \dt en psql)
  • [ ] Relaciones entre tablas correctas

✅ Fase 5: Frontend (Next.js)

  • [ ] Proyecto Next.js creado
  • [ ] TypeScript configurado
  • [ ] TailwindCSS funcionando
  • [ ] shadcn/ui inicializado
  • [ ] Componentes UI instalados (button, input, card, etc.)
  • [ ] src/lib/api.ts implementado
  • [ ] src/types/api.ts con interfaces TypeScript
  • [ ] src/hooks/useAuth.ts implementado
  • [ ] Layout principal creado
  • [ ] Página de login implementada
  • [ ] Página de registro implementada (opcional)
  • [ ] next.config.js configurado
  • [ ] Dockerfile frontend creado

✅ Fase 6: Integración

  • [ ] docker-compose.yml completo con los 3 servicios
  • [ ] Backend responde en http://localhost:8000
  • [ ] Frontend responde en http://localhost:3000
  • [ ] Swagger UI accesible en http://localhost:8000/docs
  • [ ] Frontend puede hacer requests al backend
  • [ ] CORS configurado correctamente
  • [ ] Cookies funcionando entre frontend y backend

✅ Fase 7: Funcionalidad

  • [ ] Endpoint /api/v1/health responde correctamente
  • [ ] Registro de usuario funciona (POST /api/v1/auth/register)
  • [ ] Login funciona (POST /api/v1/auth/login)
  • [ ] Token JWT se crea y almacena en cookie
  • [ ] Endpoint protegido /api/v1/users/me requiere autenticación
  • [ ] Usuario autenticado puede acceder a /api/v1/users/me
  • [ ] Logout funciona y elimina cookie
  • [ ] Frontend muestra errores apropiadamente
  • [ ] Redirección a login cuando no autenticado

✅ Fase 8: Pruebas Manuales

  • [ ] Crear usuario desde Swagger UI
  • [ ] Login con usuario creado
  • [ ] Verificar token en cookie (DevTools → Application → Cookies)
  • [ ] Acceder a endpoint protegido con token
  • [ ] Logout y verificar que cookie se elimina
  • [ ] Repetir proceso desde frontend
  • [ ] Verificar datos en PostgreSQL

✅ Fase 9: Optimización y Limpieza

  • [ ] Eliminar código de prueba/debug
  • [ ] Revisar que no hay credenciales en código
  • [ ] .env y .env.local en .gitignore
  • [ ] Documentación actualizada (README.md)
  • [ ] Comentarios en código complejo
  • [ ] Variables de entorno documentadas en .env.example

✅ Fase 10: Preparación para Producción

  • [ ] DEBUG=False en producción
  • [ ] SECRET_KEY fuerte y único
  • [ ] CORS restringido a dominios específicos
  • [ ] HTTPS habilitado (para cookies Secure)
  • [ ] Database backup configurado
  • [ ] Logs configurados apropiadamente
  • [ ] Health checks configurados
  • [ ] Rate limiting considerado
  • [ ] Monitoring y alertas (opcional)

10. TROUBLESHOOTING COMÚN

🐛 Problemas de Backend

Problema 1: "ModuleNotFoundError: No module named 'app'"

Causa: Python no encuentra el paquete app.

Solución:

# Asegurarse de estar en backend/
cd backend

# Verificar que exista app/__init__.py
ls app/__init__.py

# Ejecutar desde backend/ (no desde app/)
uvicorn app.main:app --reload

Problema 2: "sqlalchemy.exc.OperationalError: could not connect to server"

Causa: PostgreSQL no está corriendo o credenciales incorrectas.

Soluciones:

# Verificar que PostgreSQL esté corriendo
docker ps | grep postgres

# Si no está corriendo:
docker-compose up -d postgres

# Verificar logs:
docker-compose logs postgres

# Verificar variables de entorno:
cat backend/.env

# Probar conexión manual:
psql -U fastapi_user -d fullstack_db -h localhost

Problema 3: "alembic.util.exc.CommandError: Target database is not up to date"

Causa: Migraciones no aplicadas.

Solución:

cd backend
source venv/bin/activate
alembic upgrade head

Problema 4: "fastapi.exceptions.ValidationError" en endpoints

Causa: Schema Pydantic no coincide con datos enviados.

Solución:

  • Verificar el schema en la documentación Swagger (/docs)
  • Asegurarse de enviar todos los campos requeridos
  • Revisar tipos de datos (string vs int, etc.)

Problema 5: "CORS policy: No 'Access-Control-Allow-Origin'"

Causa: CORS no configurado correctamente.

Solución:

# En app/main.py, verificar:
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],  # ← Verificar URL
    allow_credentials=True,                    # ← Debe ser True para cookies
    allow_methods=["*"],
    allow_headers=["*"],
)

Problema 6: "401 Unauthorized" en endpoints protegidos

Causa: Token no enviado o inválido.

Solución:

# Verificar que cookie se creó en login
# DevTools → Network → Request a /api/v1/auth/login → Response Headers
# Debe tener: Set-Cookie: access_token=...

# Verificar que cookie se envía en requests subsecuentes
# DevTools → Network → Request a /api/v1/users/me → Request Headers
# Debe tener: Cookie: access_token=...

# Si no aparece, verificar withCredentials en axios:
# src/lib/api.ts debe tener:
# withCredentials: true

🐛 Problemas de Frontend

Problema 7: "Error: Invalid hook call"

Causa: Hook usado fuera de componente o versión React incompatible.

Solución:

  • Asegurarse de que hooks están dentro de componentes funcionales
  • Agregar 'use client' al inicio del archivo si usa hooks
'use client';  // ← Agregar esto

import { useState } from 'react';

Problema 8: "Module not found: Can't resolve '@/components/ui/button'"

Causa: shadcn/ui no instalado o path alias mal configurado.

Solución:

# Instalar componente:
npx shadcn-ui@latest add button

# Verificar tsconfig.json tenga:
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Problema 9: "Hydration failed" en Next.js

Causa: HTML del servidor no coincide con el cliente.

Solución:

  • No usar localStorage o window directamente en render
  • Usar useEffect para código que solo debe correr en cliente:
useEffect(() => {
  // Código que usa window o localStorage
}, []);

Problema 10: "TypeError: Cannot read property 'map' of undefined"

Causa: Intentar mapear un array que aún no se cargó.

Solución:

// Agregar verificación:
{data && data.map(item => (
  <div key={item.id}>{item.name}</div>
))}

// O usar optional chaining:
{data?.map(item => (
  <div key={item.id}>{item.name}</div>
))}

🐛 Problemas de Docker

Problema 11: "ERROR: Couldn't connect to Docker daemon"

Causa: Docker no está corriendo.

Solución:

# Windows/Mac: Iniciar Docker Desktop

# Linux: Iniciar servicio Docker
sudo systemctl start docker

# Verificar:
docker ps

Problema 12: "Port 5432 is already allocated"

Causa: PostgreSQL local usando el puerto.

Solución:

# Opción 1: Detener PostgreSQL local
sudo service postgresql stop  # Linux
brew services stop postgresql  # Mac

# Opción 2: Cambiar puerto en docker-compose.yml
services:
  postgres:
    ports:
      - "5433:5432"  # ← Puerto externo diferente

# Actualizar DATABASE_URL en .env:
DATABASE_URL=postgresql://...@localhost:5433/fullstack_db

Problema 13: "Cannot start service: driver failed"

Causa: Contenedor anterior no eliminado o volumen corrupto.

Solución:

# Detener y eliminar todo:
docker-compose down -v

# Eliminar contenedores manualmente:
docker rm -f fullstack_backend fullstack_frontend fullstack_postgres

# Eliminar volúmenes:
docker volume rm $(docker volume ls -q)

# Reiniciar:
docker-compose up --build

🐛 Problemas de Autenticación

Problema 14: "Cookie no se guarda en el navegador"

Causa: SameSite o Secure mal configurados.

Solución:

# En app/api/v1/endpoints/auth.py:
response.set_cookie(
    key="access_token",
    value=token.access_token,
    httponly=True,
    secure=False,      # ← False en desarrollo (localhost), True en producción con HTTPS
    samesite="lax",    # ← "lax" o "none" (no "strict" para requests cross-origin)
    max_age=1800
)

Problema 15: "Token expired"

Causa: Token JWT expiró.

Solución:

# Aumentar tiempo de expiración en .env:
ACCESS_TOKEN_EXPIRE_MINUTES=60  # 1 hora

# Implementar refresh tokens (avanzado)

🐛 Problemas de Migración

Problema 16: "Target database is not up to date"

Causa: Modelo cambió pero no se creó migración.

Solución:

alembic revision --autogenerate -m "Describe cambios"
alembic upgrade head

Problema 17: "Can't locate revision identified by 'xxxxx'"

Causa: Migración faltante o corrupta.

Solución:

# Ver historial:
alembic history

# Si falta migración, recrear desde scratch:
alembic downgrade base
rm alembic/versions/*.py
alembic revision --autogenerate -m "Initial migration"
alembic upgrade head

🐛 Problemas de Performance

Problema 18: "Backend responde muy lento"

Causas y Soluciones:

  1. Sin índices en BD:
# Agregar index=True en modelos:
email = Column(String, unique=True, index=True)
  1. N+1 queries:
# Usar eager loading:
from sqlalchemy.orm import joinedload

users = db.query(User).options(joinedload(User.projects)).all()
  1. Pool de conexiones pequeño:
# En app/db/session.py:
engine = create_engine(
    settings.DATABASE_URL,
    pool_size=20,      # ← Aumentar
    max_overflow=40    # ← Aumentar
)

📚 RECURSOS ADICIONALES

Documentación Oficial

  • FastAPI: https://fastapi.tiangolo.com/
  • Next.js: https://nextjs.org/docs
  • SQLAlchemy: https://docs.sqlalchemy.org/
  • Pydantic: https://docs.pydantic.dev/
  • TailwindCSS: https://tailwindcss.com/docs
  • shadcn/ui: https://ui.shadcn.com/

Comunidad y Soporte

  • FastAPI Discord: https://discord.gg/fastapi
  • Next.js Discord: https://nextjs.org/discord
  • Stack Overflow: Etiquetar preguntas con fastapi, next.js, sqlalchemy

Tutoriales y Cursos

  • FastAPI Official Tutorial: https://fastapi.tiangolo.com/tutorial/
  • Next.js Learn: https://nextjs.org/learn
  • Full Stack FastAPI PostgreSQL: https://github.com/tiangolo/full-stack-fastapi-postgresql

Herramientas Útiles

  • Postman: Probar APIs - https://www.postman.com/
  • pgAdmin: GUI para PostgreSQL - https://www.pgadmin.org/
  • DB Browser for SQLite: Para desarrollo rápido - https://sqlitebrowser.org/

🎉 CONCLUSIÓN

¡Felicidades! Si completaste todos los pasos, ahora tienes:

✅ Un backend FastAPI profesional con:

  • Arquitectura en capas escalable
  • Autenticación JWT segura
  • Migraciones de base de datos
  • Documentación automática

✅ Un frontend Next.js moderno con:

  • TypeScript para type safety
  • TailwindCSS para estilos
  • Componentes UI reutilizables
  • Hooks personalizados

✅ Una aplicación Full Stack completa con:

  • Comunicación segura backend-frontend
  • Autenticación con cookies HttpOnly
  • Base de datos relacional robusta
  • Todo containerizado con Docker

🚀 Próximos Pasos Recomendados

  1. Implementar CRUD Completo: Para proyectos, tareas y tags
  2. Agregar WebSockets: Para notificaciones en tiempo real
  3. Testing: Unitarios y de integración
  4. CI/CD: GitHub Actions o GitLab CI
  5. Deploy: En AWS, Google Cloud, Heroku, Railway, etc.
  6. Monitoring: Sentry, Prometheus, Grafana
  7. Features Avanzadas: Búsqueda, filtros, paginación, carga de archivos

¡El proyecto está listo para crecer! 🌟


Fin del Documento 4