Desarrollo: Aplicación profesional de soporte técnico. Un resumen con IA.
Autor: Marcelo Robin (contenido original en video), IA (en la generación de texto)
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
-
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
-
Sobre PostgreSQL:
- Usar pool de conexiones para mejor rendimiento
- Configurar
pool_pre_ping=Truepara reconexiones automáticas - Siempre cerrar sesiones de base de datos con
try/finally
-
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/awaitpara operaciones I/O bound
-
Sobre contenedores:
- Usar volúmenes para persistencia de datos
- Configurar networks para comunicación entre contenedores
- Separar configuraciones de desarrollo y producción
-
Sobre seguridad:
- NUNCA commitear archivos
.enval repositorio - Usar
SECRET_KEYfuertes generadas aleatoriamente - Configurar CORS apropiadamente según el entorno
- NUNCA commitear archivos
🎬 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
-
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
-
Sobre TypeScript:
- Aprovechar el tipado fuerte para prevenir errores
- Generar tipos automáticamente desde OpenAPI del backend
- Usar interfaces para estructuras de datos complejas
-
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
-
Sobre la comunicación Frontend-Backend:
- Usar
withCredentials: truepara cookies HttpOnly - Configurar CORS apropiadamente en ambos lados
- Manejar errores de red con interceptors de axios
- Usar
-
Sobre contenedores:
- Multi-stage builds reducen el tamaño de la imagen final
- Usar
standaloneoutput 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
-
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
-
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
-
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
-
Sobre Excepciones Personalizadas:
- Proporciona mensajes de error consistentes
- Facilita el manejo centralizado de errores
- Mejora la experiencia del cliente de la API
-
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
-
Sobre Relaciones SQLAlchemy:
back_populatescrea relaciones bidireccionales automáticascascade="all, delete-orphan"elimina registros huérfanos automáticamentelazy="dynamic"devuelve una query en lugar de cargar todos los datosondelete="CASCADE"o"SET NULL"define comportamiento a nivel de BD
-
Sobre Foreign Keys:
- Siempre indexar foreign keys para mejor rendimiento
- Definir explícitamente el comportamiento
ondelete - Usar nombres descriptivos para las columnas FK
-
Sobre Enums:
- SQLAlchemy Enums proporcionan validación a nivel de BD
- Usar
str, enum.Enumpara compatibilidad con Pydantic - Los enums mejoran la integridad de datos
-
Sobre Timestamps:
- Usar
default=datetime.utcnow(sin paréntesis) para llamada lazy onupdateactualiza automáticamente el campo en cada UPDATE- Siempre trabajar con UTC en el backend
- Usar
-
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
-
Sobre Pydantic Schemas:
- Separar schemas de creación, actualización y respuesta
- Usar
Optionalpara campos que pueden no estar presentes en updates from_attributes = Truepermite 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
- Episodio 1: Visión general y arquitectura
- Episodio 2: Backend inicial (el más extenso)
- Episodio 3: Frontend con Next.js
- Episodio 4: Refactorización profesional
- 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
- Introducción al Proyecto Completo
- Arquitectura General de la Aplicación
- Configuración del Entorno
- Estructura Profesional del Proyecto
- Backend con FastAPI
- Frontend con Next.js
- Modelos y Relaciones de Datos
- Implementación de WebSockets
- Autenticación y Seguridad
- Mejores Prácticas Aplicadas
- 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
- Separación de Responsabilidades: Cada módulo tiene una función específica
- Modularidad: Código organizado en paquetes reutilizables
- Escalabilidad: Fácil agregar nuevas características
- Testabilidad: Estructura facilita pruebas unitarias
- 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 Pydanticsecurity.py: JWT, hashing, autenticaciónexceptions.py: Excepciones personalizadas
app/api/ - Capa de presentación
deps.py: Dependencias compartidasv1/router.py: Router principal versionadov1/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
- Component-Based: Todo es un componente reutilizable
- Type Safety: TypeScript en todo el código
- Separation of Concerns: Lógica separada de presentación
- Performance: Server Components por defecto
- 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/uilayout/: Header, Footer, Sidebarforms/: Formularios específicos
src/lib/ - Librerías y utilidades
api.ts: Cliente HTTP (axios)auth.ts: Lógica de autenticaciónutils.ts: Funciones auxiliares
src/hooks/ - React Hooks personalizados
useAuth: Gestión de autenticaciónuseWebSocket: Conexión WebSocketuseApi: 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 IDiat: 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
-
Variables de Entorno
- Nunca commitear archivos
.env - Usar secretos diferentes por entorno
- Rotar secretos periódicamente
- Nunca commitear archivos
-
HTTPS en Producción
- Forzar HTTPS
- HSTS headers
- Certificados SSL/TLS válidos
-
Rate Limiting
- Limitar intentos de login
- Prevenir brute force attacks
- Usar herramientas como slowapi
-
Logging y Monitoreo
- Log de intentos de autenticación
- Alertas de actividad sospechosa
- No logear información sensible
-
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
- Episodio 1: Introducción al Proyecto Full Stack
- Episodio 2: Preparación del Backend con FastAPI
- Episodio 3: Configuración del Frontend con Next.js
- Episodio 4: Reorganización Profesional del Backend
- 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:
- ✅ Introducir el stack tecnológico (FastAPI + Next.js + PostgreSQL + Podman)
- ✅ Explicar la arquitectura general de la aplicación
- ✅ Destacar características clave: WebSockets y autenticación segura
- ✅ Motivar el aprendizaje mostrando un proyecto real
- ✅ 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:
-
Comprensión Clara del Proyecto:
- Entienden qué se va a construir
- Conocen las tecnologías involucradas
- Tienen expectativas realistas del alcance
-
Motivación Establecida:
- Ven el valor de un proyecto profesional
- Comprenden las aplicaciones prácticas
- Están preparados para el compromiso de la serie
-
Contexto Técnico:
- Conocen las ventajas de cada tecnología
- Entienden por qué se eligió este stack
- Ven cómo las piezas encajan juntas
-
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:
- ✅ Instalar Python 3.11+ y crear entorno virtual
- ✅ Instalar FastAPI, Uvicorn y todas las dependencias necesarias
- ✅ Crear la estructura inicial del proyecto backend
- ✅ Configurar PostgreSQL y establecer conexión con SQLAlchemy
- ✅ Implementar configuración con variables de entorno
- ✅ Crear la aplicación FastAPI con middleware CORS
- ✅ Implementar endpoints básicos de prueba y health check
- ✅ Configurar Dockerfile y docker-compose para el backend
- ✅ 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 principaluvicorn[standard]: Servidor ASGI con extrassqlalchemy: ORM para PostgreSQLpsycopg2-binary: Driver de PostgreSQLalembic: Migraciones de base de datospydantic-settings: Configuración con validaciónpython-jose[cryptography]: JWTpasslib[bcrypt]: Hashing de contraseñaspython-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=Truepara 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:
-
Backend Funcional:
- Servidor FastAPI corriendo en puerto 8000
- Documentación automática accesible en /docs
- Health check endpoint respondiendo
-
Base de Datos Configurada:
- PostgreSQL ejecutándose en contenedor
- Conexión establecida con SQLAlchemy
- Pool de conexiones configurado
-
Estructura Profesional:
- Código organizado en módulos
- Variables de entorno separadas
- Configuración centralizada con Pydantic
-
Entorno Contenerizado:
- Docker Compose orquestando servicios
- Volúmenes para persistencia de datos
- Hot reload para desarrollo ágil
-
Middleware Configurado:
- CORS listo para Next.js
- Preparado para agregar más middleware
-
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
--reloadde 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
localhosten lugar depostgres.
🎬 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
- Sobre Uvicorn: El flag
--reloades solo para desarrollo, nunca en producción - Sobre Pool de Conexiones: Ajustar
pool_sizesegún carga esperada - Sobre CORS: En producción, especificar origins exactos, no usar "*"
- Sobre .env: Generar SECRET_KEY con
openssl rand -hex 32 - 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:
- ✅ Crear proyecto Next.js con App Router y TypeScript
- ✅ Configurar TailwindCSS para estilos utility-first
- ✅ Instalar y configurar shadcn/ui para componentes
- ✅ Estructurar el proyecto con buenas prácticas
- ✅ Implementar cliente API con axios
- ✅ Configurar variables de entorno para URLs
- ✅ Crear contenedor Docker para Next.js
- ✅ Integrar frontend y backend en docker-compose
- ✅ 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: truepara 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:
-
Frontend Funcional:
- Next.js corriendo en puerto 3000
- App Router configurado
- TypeScript funcionando
-
Estilos Configurados:
- TailwindCSS listo para usar
- Sistema de diseño con variables CSS
- Dark mode preparado
-
Componentes UI:
- shadcn/ui instalado
- Botones, inputs, cards, etc. disponibles
- Personalizables y accesibles
-
Comunicación con Backend:
- Cliente API configurado con axios
- Variables de entorno para URLs
- Interceptors para manejo de errores
-
Estructura Escalable:
- Carpetas organizadas lógicamente
- Separación de concerns
- Fácil de navegar y extender
-
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
- Sobre shadcn/ui: Los componentes se copian al proyecto, así que puedes modificarlos libremente
- Sobre TypeScript: Usar
interfacepara objetos públicos,typepara uniones y aliases - Sobre Tailwind: Usar el plugin de VSCode para autocompletado de clases
- Sobre API Client: Centralizar todas las llamadas en lib/api.ts facilita mantenimiento
- 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:
- ✅ Implementar arquitectura en capas (API → Services → Repositories → Models)
- ✅ Crear patrón Repository con CRUD genérico
- ✅ Implementar Service Layer para lógica de negocio
- ✅ Definir excepciones personalizadas
- ✅ Mejorar sistema de dependencias (deps.py)
- ✅ Versionar la API (prefijo /api/v1)
- ✅ Refactorizar endpoints para usar nueva arquitectura
- ✅ 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_userdepende deget_current_userget_current_userdepende deget_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:
-
Arquitectura Profesional:
- 4 capas bien definidas
- Separación clara de responsabilidades
- Código mantenible y escalable
-
Repository Pattern:
- CRUD genérico funcionando
- Repositorios específicos por entidad
- Abstracción completa de SQLAlchemy
-
Service Layer:
- Lógica de negocio centralizada
- Validaciones complejas
- Servicios reutilizables
-
Manejo de Errores:
- Excepciones personalizadas
- Códigos HTTP apropiados
- Mensajes de error consistentes
-
API Versionada:
- Estructura para versiones futuras
- Documentación organizada por versión
- Evolución controlada
-
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
- Sobre Refactoring: Hacer en pasos pequeños, probando después de cada cambio
- Sobre Repositories: Empezar con CRUD genérico, agregar métodos específicos según necesidad
- Sobre Services: Un servicio por entidad principal (UserService, ProjectService, etc.)
- Sobre Exceptions: Crear solo las que realmente necesitas, no todas las posibles
- 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:
- ✅ Diseñar esquema ER (Entidad-Relación) completo
- ✅ Crear modelo base con timestamps automáticos
- ✅ Implementar modelo User completo
- ✅ Implementar modelo Project con relación 1:N a User
- ✅ Implementar modelo Task con relaciones múltiples
- ✅ Implementar modelo Tag con relación N:M a Task
- ✅ Configurar Alembic para migraciones
- ✅ Crear esquemas Pydantic correspondientes
- ✅ 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_aten todos los modelos automáticamente datetime.utcnowsin()para lazy evaluationonupdateactualizaupdated_aten cada UPDATE__abstract__ = Trueevita crear tabla para BaseModeldeclared_attrpara 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=Trueen username y emailindex=Trueen campos de búsqueda frecuentecascade="all, delete-orphan"elimina proyectos si se elimina usuariolazy="dynamic"para cargar relaciones bajo demandaforeign_keysexplí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=Falseenowner_id- Proyecto DEBE tener un owner
- No puede existir proyecto huérfano
index=Trueen foreign keys- Acelera queries
WHERE owner_id = X - Crucial para performance
- Acelera queries
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.Enumpara compatibilidad Pydantic- Previene datos inválidos
- Mejor que strings libres
- ondelete Diferenciados:
CASCADEpara project: eliminar task si se elimina proyectoSET NULLpara 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_tagssin 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:
-
Esquema de BD Completo:
- 4 tablas principales (users, projects, tasks, tags)
- 1 tabla intermedia (task_tags)
- Todas las relaciones configuradas
-
Modelos SQLAlchemy:
- Modelo base reutilizable
- Timestamps automáticos
- Relaciones bidireccionales
- Cascadas y comportamientos configurados
-
Esquemas Pydantic:
- Schemas de creación (Create)
- Schemas de actualización (Update)
- Schemas de respuesta (Response)
- Validaciones con Field()
-
Sistema de Migraciones:
- Alembic configurado
- Migración inicial creada
- Esquema aplicado en PostgreSQL
-
Features Avanzadas:
- Enums para estados
- Relaciones opcionales y requeridas
- Tabla intermedia para N:M
- Índices para performance
-
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"vsondelete="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
-
Sobre Diseño de Esquema: Dibuja el diagrama ER antes de codificar. Ayuda a visualizar relaciones.
-
Sobre Migraciones: Usa mensajes descriptivos. Serás tú en el futuro quien los lea.
-
Sobre Enums: Define enums en el modelo SQLAlchemy. Pydantic los importa automáticamente.
-
Sobre Relaciones:
back_populatesdebe coincidir en ambos modelos o fallará. -
Sobre Índices: Indexar foreign keys es crucial. PostgreSQL no lo hace automáticamente.
-
Sobre Timestamps: Siempre usar UTC (
datetime.utcnow). Convertir a timezone local en el frontend. -
Sobre Cascadas: Documentar decisiones de
ondeleteen 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:
- Visión → Sabes QUÉ vas a construir
- Backend → Tienes un servidor funcionando
- Frontend → Tienes una UI que consume la API
- Arquitectura → Código profesional y escalable
- 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
- Requisitos Previos y Herramientas
- Instalación y Configuración del Entorno
- Configuración del Backend (FastAPI)
- Configuración del Frontend (Next.js)
- Configuración de la Base de Datos
- Estructura Completa de Archivos
- Código Completo por Módulos
- Comandos de Terminal Necesarios
- Checklist de Verificación
- 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
- [ ]
.gitignorecreado con contenido apropiado - [ ] Estructura de carpetas creada (
backend/,frontend/) - [ ]
backend/.envconfigurado con variables correctas - [ ]
frontend/.env.localconfigurado - [ ]
docker-compose.ymlcreado en raíz
✅ Fase 3: Backend (FastAPI)
- [ ] Entorno virtual creado y activado
- [ ] Dependencias instaladas (
pip listmuestra paquetes) - [ ]
requirements.txtcreado con todas las dependencias - [ ] Estructura de carpetas backend creada correctamente
- [ ] Archivos
__init__.pyen todos los paquetes - [ ]
app/main.pyimplementado - [ ]
app/core/config.pycon Settings configurado - [ ]
app/core/security.pycon funciones JWT - [ ]
app/db/session.pycon 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.pymodificado correctamente - [ ] Dockerfile backend creado
✅ Fase 4: Base de Datos
- [ ] PostgreSQL corriendo (Docker o local)
- [ ] Base de datos
fullstack_dbcreada - [ ] Usuario
fastapi_usercreado 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
\dten 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.tsimplementado - [ ]
src/types/api.tscon interfaces TypeScript - [ ]
src/hooks/useAuth.tsimplementado - [ ] Layout principal creado
- [ ] Página de login implementada
- [ ] Página de registro implementada (opcional)
- [ ]
next.config.jsconfigurado - [ ] Dockerfile frontend creado
✅ Fase 6: Integración
- [ ]
docker-compose.ymlcompleto 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/healthresponde 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/merequiere 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
- [ ]
.envy.env.localen.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=Falseen producción - [ ]
SECRET_KEYfuerte 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
localStorageowindowdirectamente en render - Usar
useEffectpara 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:
- Sin índices en BD:
# Agregar index=True en modelos:
email = Column(String, unique=True, index=True)
- N+1 queries:
# Usar eager loading:
from sqlalchemy.orm import joinedload
users = db.query(User).options(joinedload(User.projects)).all()
- 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
- Implementar CRUD Completo: Para proyectos, tareas y tags
- Agregar WebSockets: Para notificaciones en tiempo real
- Testing: Unitarios y de integración
- CI/CD: GitHub Actions o GitLab CI
- Deploy: En AWS, Google Cloud, Heroku, Railway, etc.
- Monitoring: Sentry, Prometheus, Grafana
- Features Avanzadas: Búsqueda, filtros, paginación, carga de archivos
¡El proyecto está listo para crecer! 🌟
Fin del Documento 4