Comparativa de los 5 mejores stacks para desarrollo web
Intro ¶
El popular youtuber Theo - t3․gg ha creado un video que llevaba tiempo queriendo hacer: desarrollar la misma aplicación usando cinco stacks tecnológicos diferentes. Desde Rails hasta Elixir/Phoenix, pasando por Go con GraphQL, T3 Stack y React Server Components. En este análisis te mostramos su experiencia, evaluando el rendimiento, escalabilidad y experiencia de desarrollo para ayudarte a tomar la mejor decisión en tu próximo proyecto.
5 para llevar ¶
1. No hay un stack perfecto; cada tecnología tiene su contexto ideal.
Rails es rápido para prototipos, Elixir brilla en tiempo real, GraphQL es poderoso para APIs escalables, T3 Stack ofrece tipado completo y simplicidad, mientras que React Server Components maximiza el rendimiento con renderizado del servidor.
2. El rendimiento depende tanto del stack como de las optimizaciones específicas.
La pre-carga de imágenes en Elixir y React Server Components mostró mejoras claras en la velocidad. Por otro lado, los problemas de cold start en T3 Stack y la complejidad de configuración en GraphQL afectan negativamente la experiencia.
3. La experiencia del desarrollador (DX) es clave para la adopción de un stack.
Rails y Go pueden ser frustrantes debido a la cantidad de archivos y configuración, mientras que stacks como T3 y React Server Components mejoran la experiencia con TypeScript y caché integrado, simplificando el desarrollo.
4. El tipado estático mejora la seguridad y reduce errores, pero puede ser difícil de configurar.
GraphQL con Apollo tiene una configuración compleja para el tipado estático, mientras que T3 Stack y React Server Components ofrecen una experiencia más fluida y segura gracias a TypeScript.
5. Los React Server Components simplifican el desarrollo y mantenimiento al minimizar el código en el cliente.
Al enfocarse en el renderizado del servidor, se logra una mejor integración de datos y se reduce la complejidad, resultando en aplicaciones más rápidas y fáciles de mantener.
Los 5 stacks ¶
Las cinco tecnologías diferentes que utiliza el autor para desarrollar la misma aplicación (“Roundest Pokémon”) son:
-
Ruby on Rails: Un framework web basado en Ruby que sigue el patrón MVC (Modelo-Vista-Controlador). Es conocido por su facilidad de uso y sus convenciones, pero puede volverse complejo por la gran cantidad de archivos generados y la rigidez de su estructura.
-
Elixir (Phoenix y LiveView): Usa el lenguaje Elixir junto con el framework Phoenix y su extensión LiveView. Este stack aprovecha la concurrencia de Elixir y permite crear interfaces dinámicas en tiempo real usando WebSockets, sin necesidad de mucho JavaScript.
-
Go (con GraphQL): Utiliza el lenguaje Go (Golang) para crear el backend, implementando una API GraphQL. La API permite consultas flexibles y tipadas, separando el frontend y backend de manera estricta. La integración con Apollo Client en el frontend facilita la gestión de datos.
-
T3 Stack (Next.js, TRPC, Prisma): Un stack moderno basado en TypeScript que utiliza Next.js para el frontend, TRPC para llamadas de API tipadas y Prisma como ORM para la base de datos. La combinación ofrece una experiencia de desarrollo fluida y centrada en el tipado estático.
-
React Server Components (RSC) con Next.js App Router: Una implementación moderna utilizando React Server Components y el App Router de Next.js. Permite cargar componentes del lado del servidor, minimizando el JavaScript en el cliente y mejorando el rendimiento general de la aplicación.
Resumen de los stacks:
- Rails: Tradicional, MVC, mucho código generado automáticamente.
- Elixir/Phoenix: Funcional, concurrente, tiempo real con LiveView.
- Go/GraphQL: Backend fuerte con API GraphQL, separación clara entre frontend y backend.
- T3 Stack: Integrado, tipado completo, centrado en TypeScript.
- React Server Components: Minimalista, enfocado en servidor, menos código en el cliente.
Aquí hay algo que podría hacer cambiar tu futuro.
Usamos cookies de terceros para mostrar este iframe (que no es de publicidad ;).
La webapp que vamos a construir ¶
El creador del video ha desarrollado una aplicación sencilla llamada “Roundest Pokémon”. El objetivo de la app es determinar cuál es el Pokémon más “redondo”, enfrentando a dos Pokémon al azar y permitiendo a los usuarios votar cuál creen que es el más redondo. Los votos se registran y luego se pueden ver los resultados agregados.
Detalles de la aplicación: ¶
- Pantalla principal: muestra dos Pokémon al azar para que los usuarios elijan cuál consideran más redondo.
- Registro de votos: cada vez que el usuario elige un Pokémon, se registra el voto y se muestra un nuevo par.
- Página de resultados: muestra estadísticas de los votos, incluyendo el porcentaje de victorias para cada Pokémon.
Características técnicas: ¶
- La aplicación utiliza diferentes stacks para probar el rendimiento, la facilidad de uso y la experiencia de desarrollo en cada tecnología.
- Implementa técnicas de optimización, como pre-carga de imágenes y uso de cookies para almacenar datos temporales.
- Se explora tanto la implementación tradicional (con APIs REST y GraphQL) como enfoques más modernos (React Server Components y cacheo agresivo).
Guía completa del vídeo para elegir el mejor stack ¶
Por qué Rails sigue siendo relevante en 2024 ¶
Después de una década trabajando con diferentes stacks tecnológicos - desde Rails hasta Elixir, Go y T3 - decidí hacer un experimento interesante: construir la misma aplicación cinco veces usando cada una de estas tecnologías.
Rendimiento sorprendente en Rails ¶
La primera prueba fue con Rails, y aunque inicialmente tenía mis dudas, hubo algunas sorpresas positivas:
- El rendimiento fue notablemente más rápido de lo esperado, especialmente considerando que se ejecutaba en el tier gratuito de fly.io
- La aplicación maneja múltiples peticiones rápidamente, incluso realizando dos requests por cada acción (un POST para el voto y una recarga de página)
La complejidad de la estructura MVC ¶
Sin embargo, la estructura del proyecto reveló algunos desafíos:
- La cantidad de carpetas y archivos generados automáticamente es abrumadora
- Se crean múltiples recursos que podrían no ser necesarios:
- Archivos para mailers en varias ubicaciones
- Carpetas de jobs
- Helpers posiblemente sin usar
- Estructura JavaScript independiente
Para desarrolladores que vienen del mundo del full stack TypeScript moderno, donde el patrón MVC ha quedado en gran medida atrás, puede resultar desconcertante la cantidad de archivos que hay que tocar para realizar cambios simples.
# Ejemplo de estructura típica de Rails
/app
/controllers
/models
/views
/layouts
/mailers
/helpers
/jobs
/javascript
Los desafíos del desarrollo moderno con Rails ¶
Siguiendo con los desafíos encontrados al trabajar con Rails en 2024, la experiencia de desarrollo presenta varios puntos de fricción:
Navegación y debugging ¶
A diferencia de los entornos de desarrollo modernos, Rails presenta algunas limitaciones:
- La navegación entre archivos es poco intuitiva:
- El comando “command-click” no funciona como se esperaría
- Los links llevan a archivos internos de las gemas en lugar del código fuente
- La verificación de errores requiere múltiples pasos:
- Ejecutar comandos localmente
- Navegar manualmente entre archivos
- Verificar resultados en el navegador
Complejidad del proyecto ¶
La estructura mencionada anteriormente se traduce en números concretos:
- Más de 1000 líneas de código en un proyecto básico
- 83 archivos distribuidos en:
- 40 archivos Ruby
- 11 templates ERB
- 3 archivos HTML
- Muchos archivos contienen solo 5 líneas de código pero son críticos para el funcionamiento
Problemas de configuración ¶
La experiencia de configuración inicial presenta varios obstáculos:
# Ejemplo de problemas comunes
brew install postgresql # Versión incorrecta en docs oficiales
# Ubicación en M1/M2 Macs
/opt/homebrew/bin # Nueva ubicación
/usr/local/bin # Ubicación en docs antiguas
- Documentación desactualizada en la configuración oficial para Mac
- Stack Overflow lleno de respuestas obsoletas de hace más de 10 años
- Problemas con la configuración de PostgreSQL
- Modificaciones no deseadas en archivos
.zshrc
- Tiempos de instalación de más de 6 minutos
- Dificultades en la integración con Tailwind CSS en proyectos existentes
La magia y las peculiaridades de Rails ¶
Como vimos en las secciones anteriores, Rails tiene sus complejidades. Sin embargo, también ofrece algunas características notables y otras peculiaridades interesantes:
La experiencia de desarrollo mejorada ¶
La extensión de VS Code para Ruby (desarrollada por Shopify) aporta mejoras significativas:
- Navegación inteligente con command-click para modelos y relaciones
- Integración con Active Record para mostrar relaciones entre modelos
- Sin embargo, presenta algunas molestias:
- Cambia el tema del editor por defecto al instalarse
- La navegación no funciona en archivos ERB
Active Record: el ORM pionero ¶
Active Record destaca como uno de los primeros y más influyentes ORMs:
# Ejemplo de migración en Rails
class AddFieldsToPokemon < ActiveRecord::Migration[7.0]
def change
add_column :pokemons, :name, :string
add_column :pokemons, :dex_id, :integer
add_column :pokemons, :sprite, :string
end
end
Gestión del esquema de base de datos ¶
Rails implementa un enfoque único para mantener la integridad del esquema:
- El archivo
schema.rb
se autogenera a partir del estado de la base de datos - La fuente de verdad son las migraciones, no el esquema
- No se permite editar el esquema directamente
- Las modificaciones se realizan exclusivamente a través de migraciones
Convención sobre configuración ¶
Rails utiliza convenciones interesantes:
- El nombre de la migración determina el modelo a modificar
- Los archivos se duplican (migraciones y esquema) pero mantienen la consistencia
- La “magia” de Rails permite inferir relaciones y nombres, aunque puede resultar confusa para nuevos desarrolladores
Esta filosofía de “convención sobre configuración”, que mencionamos al principio del post, se hace evidente en estas características, aunque a veces puede resultar en comportamientos poco intuitivos.
El despliegue con Fly.io: un respiro en la complejidad ¶
Después de explorar los desafíos de Rails con sus migraciones y convenciones mágicas, encontramos un aspecto sorprendentemente positivo en el proceso de despliegue.
La simplicidad del despliegue ¶
Fly.io ofrece una experiencia notablemente fluida:
- Configuración automática sin necesidad de archivos personalizados
- Detección inteligente de requisitos:
- Reconoce automáticamente la necesidad de PostgreSQL
- Configura variables de ambiente
- Gestiona despliegues programados
# Proceso de despliegue simplificado
fly launch
fly deploy
Características inteligentes de la plataforma ¶
El sistema implementa varias optimizaciones:
- Selección automática de región basada en ping desde el CLI
- Sistema de caché para actualizaciones rápidas:
- Primera instalación más lenta por la instalación de gemas
- Actualizaciones posteriores “hilariantemente rápidas”
- Uso eficiente de imágenes Docker
Gestión eficiente de recursos ¶
A diferencia de las soluciones serverless tradicionales:
- Hibernación inteligente cuando no hay tráfico
- Activación bajo demanda al recibir solicitudes
- Costos optimizados:
- 33 centavos utilizados de $5 de crédito mensual
- Múltiples proyectos con costo mínimo
- VPS que se apagan cuando no están en uso
Este enfoque equilibra la eficiencia de recursos con la disponibilidad, ofreciendo una solución más práctica que las alternativas tradicionales o puramente serverless que mencionamos en secciones anteriores.
Phoenix y Elixir: velocidad y elegancia moderna ¶
Después de explorar Rails y su despliegue en Fly.io, pasamos a una tecnología más moderna que busca traer lo mejor de Ruby a la era actual.
La promesa de Elixir y Phoenix ¶
Elixir, creado por José Valim, representa un enfoque moderno:
- Combina la elegancia de Ruby con programación funcional
- Incorpora patrones de concurrencia potentes
- Phoenix actúa como el framework web, similar al rol de Rails
LiveView: el diferencial de Phoenix ¶
La velocidad de la aplicación se debe a LiveView, que ofrece:
# Ejemplo de comunicación WebSocket en LiveView
# Mensaje enviado
{
type: "click",
event: "vote",
value: "winner_loser_pair"
}
# Respuesta con diff
{
status: "ok",
response: {
diff: {
# Cambios específicos en el DOM
}
}
}
Características destacadas: ¶
- Conexión WebSocket persistente en lugar de peticiones HTTP
- Actualizaciones incrementales mediante diffs
- Optimización de imágenes con precarga
- Respuesta casi instantánea en la interfaz
Rendimiento optimizado ¶
La implementación muestra mejoras significativas:
- Actualizaciones instantáneas de la interfaz
- Sistema de diff inteligente que minimiza datos transferidos
- Precarga de imágenes para eliminar retrasos visuales
- Posiblemente la versión más rápida de todas las implementaciones probadas
Este enfoque moderno contrasta significativamente con la arquitectura tradicional de Rails que vimos anteriormente, ofreciendo una experiencia más fluida y rápida.
La anatomía de un proyecto Phoenix ¶
Comparando con la estructura de Rails que analizamos anteriormente, Phoenix presenta un enfoque diferente pero con algunas similitudes interesantes.
Métricas de código ¶
La comparativa con Rails revela diferencias significativas:
- Phoenix:
- 47 archivos totales (31 Elixir)
- 1,395 líneas de código (con optimizaciones)
- 832 líneas de Elixir base
- Rails (como vimos antes):
- 83 archivos totales
- 1,000 líneas de código
Estructura del proyecto ¶
El proyecto mantiene una separación clara de responsabilidades:
# Ejemplo de estructura de directorios
/lib # Lógica de la aplicación
/roundest # Código principal
/controllers
/routes
/priv # Datos y recursos privados
/repo
/migrations
Filosofía funcional ¶
- Separación entre código (lib) y datos (priv)
- Enfoque en programación funcional:
- Código sin estado en
lib
- Estado contenido en
priv
- Código sin estado en
Desafíos encontrados ¶
A pesar de su elegancia, surgieron varios obstáculos:
- Gestión de seeds:
- Las seeds por defecto solo funcionan en desarrollo
- Necesidad de crear módulos específicos para producción
- Requiere SSH y evaluación de código para sembrar datos
# Módulo necesario para seeds en producción
defmodule GlobalSetup do
def run_seed do
# Código de seed
end
end
- Variables de entorno:
- Problemas con la configuración de base de datos
- Conflictos con el LSP del editor
- Dificultades con PostgreSQL en desarrollo local
Estos desafíos muestran que, aunque Phoenix moderniza muchos conceptos de Rails, todavía mantiene algunas complejidades propias.
La elegancia y complejidad de Elixir ¶
Continuando con nuestro análisis de Phoenix, vemos que bajo la complejidad inicial yacen características elegantes que lo diferencian de Rails.
Configuración y estructura ¶
La complejidad de configuración se mantiene:
# Múltiples archivos de configuración
/config
config.exs
dev.exs
prod.runtime.exs
test.exs
Desafíos de organización: ¶
- Múltiples archivos de configuración
- Dificultad para identificar código generado vs personalizado
- Problemas con formateo de templates
Las joyas de Elixir ¶
El lenguaje brilla con características únicas:
Pipes ¶
# Sin pipes
assign(socket, field1, value1)
|> assign(field2, value2)
# Con pipes
socket
|> assign(field1, value1)
|> assign(field2, value2)
Pattern Matching en funciones ¶
# Pattern matching para diferentes estados
def render(%{page: "loading"}) do
# Renderiza estado de carga
end
def render(assigns) do
# Renderiza estado normal
end
Características avanzadas ¶
-
Ejecución asíncrona:
- Uso de
Task.start
para operaciones no bloqueantes - Votación asíncrona sin bloquear la UI
- Uso de
-
Gestión de estado:
- Pattern matching para controlar diferentes estados
- Manejo elegante de estados de carga
- Sistema de montaje doble en LiveView
Estas características muestran cómo Elixir moderniza conceptos que vimos en Rails, aunque mantiene cierta complejidad en su configuración y estructura.
LiveView: un nuevo paradigma de UI ¶
Profundizando en el funcionamiento de LiveView, descubrimos un enfoque radicalmente diferente al que vimos en Rails.
El ciclo de vida de LiveView ¶
Montaje inicial ¶
# Gestión de estados en el montaje
def mount(_params, _session, socket) do
case connected?(socket) do
true ->
{:ok, assign(socket, random_pair())}
false ->
{:ok, assign(socket, page: "loading")}
end
end
- Doble montaje:
- Renderizado inicial en servidor
- Conexión WebSocket posterior
- Solución elegante:
- Estado de carga inicial
- Actualización cuando se establece la conexión
Gestión de votos y estado ¶
def record_vote(socket, winner_id) do
case {first_entry.id == winner_id} do
true ->
# Actualización de votos
Repo.transaction do
# Actualización del ganador y perdedor
end
end
end
Características clave: ¶
- Transacciones atómicas para votos
- Pattern matching para determinar ganador/perdedor
- Actualización automática de la UI
El paradigma sin estado en el cliente ¶
La UI se actualiza automáticamente:
- No hay estado en el cliente
- Las asignaciones (
assigns
) disparan rerenderizado - Sincronización automática servidor-cliente
# Template sin estado
<button phx-click="vote" phx-value={@winner_id}>
<%= @pokemon.name %>
</button>
Este enfoque contrasta significativamente con el modelo tradicional de Rails que vimos al principio, eliminando la necesidad de gestionar estado en el cliente y simplificando la sincronización de datos.
Optimización y organización en Phoenix ¶
Profundizando en las optimizaciones y la estructura del código, Phoenix revela un enfoque funcional elegante que contrasta con el estilo MVC de Rails.
Precarga de imágenes ¶
# Implementación de precarga
<div class="hidden">
<img src={@next_first_entry.sprite} />
<img src={@next_second_entry.sprite} />
</div>
Características de la versión turbo: ¶
- Imágenes ocultas para precarga
- Gestión asíncrona de votos con
Task.start
- Actualización inmediata de la UI
Manejo de eventos ¶
def handle_event("vote", %{"value" => winner_id}, socket) do
Task.start(fn -> record_vote(winner_id) end)
# Actualización inmediata de la UI
{:noreply, assign(socket,
first_entry: socket.assigns.next_first,
second_entry: socket.assigns.next_second,
next_first: new_first,
next_second: new_second
)}
end
Simplificación de la estructura ¶
A diferencia de Rails, el código se concentra en menos archivos:
- De 31 archivos Elixir (47 totales), solo 7 son esenciales
- La mayoría de la lógica reside en archivos
live
- Router centralizado con configuración clara:
- Pipelines para sesiones
- Live flash
- Layouts
- Rutas personalizables
Este enfoque funcional ofrece:
- Estado predecible
- UI como resultado lógico del estado
- Configuración flexible en código Elixir
- Menor fragmentación del código
Esta organización representa una evolución significativa respecto al modelo MVC tradicional que vimos en Rails, ofreciendo mayor cohesión y menor dispersión del código.
GraphQL y Go: el contraste moderno ¶
Después de explorar la elegancia de Phoenix y Elixir, pasamos a una arquitectura radicalmente diferente con Go y GraphQL.
La experiencia GraphQL ¶
# Query ejemplo
query Pokemon {
randomPair {
pokemon1 {
id
name
}
pokemon2 {
id
name
}
}
}
# Mutación para votos
mutation Vote($upvote: ID!, $downvote: ID!) {
vote(upvoteId: $upvote, downvoteId: $downvote) {
success
}
}
Ventajas clave: ¶
- GraphQL Explorer integrado
- Queries personalizables en tiempo real
- Documentación automática
- Navegación cliente instantánea
Rendimiento y arquitectura ¶
Comparado con la solución Phoenix:
- Velocidad ligeramente inferior
- Navegación instantánea en cliente
- Separación clara entre cliente y servidor
- Uso de Apollo Client:
useQuery
yuseMutation
- Gestión de caché automática
- Inspiración para React Query y otros
Este enfoque representa un contraste significativo con:
- El modelo monolítico de Rails
- La arquitectura websocket de Phoenix
- La integración servidor-cliente que vimos anteriormente
La combinación de Go y GraphQL ofrece una arquitectura más moderna y desacoplada, aunque sacrifica algo de la velocidad que vimos en la implementación con Phoenix y LiveView.
Los desafíos de GraphQL y TypeScript ¶
Tras ver las ventajas de GraphQL, nos encontramos con una serie de desafíos significativos en su implementación moderna.
Implementación básica vs. tipado ¶
// Implementación básica (20 minutos)
const POKE_QUERY = gql`
query Pokemon {
randomPair {
pokemon1 {
id
name
}
}
}
`;
// Problemas con template literals y tipos
const POKE_QUERY = /* graphql */ `
query Pokemon {
# ...
}
` as const;
Desafíos de configuración: ¶
- Implementación inicial rápida (20 minutos)
- Generación de tipos compleja (4+ horas)
- Problemas con template literals
- Conflictos de formateo con Prettier
Problemas técnicos específicos ¶
-
Template literals:
- Error de tipos con tag
gql
- Necesidad de comentario
/* graphql */
- Problemas de autocompletado
- Error de tipos con tag
-
Prettier y formato:
- Requiere configuración especial
- Conflictos con indentación
- Necesidad de comentarios específicos
Configuración de tipos ¶
// Configuración compleja de codegen
// Horas de configuración y debugging
// Problemas con documentación inconsistente
Este contraste con la simplicidad que vimos en Phoenix LiveView muestra cómo la adición de tipado estricto y herramientas modernas puede complicar significativamente el desarrollo, a pesar de las ventajas que ofrece GraphQL en términos de flexibilidad y exploración de datos.
GraphQL y Go: definiendo la API ¶
Profundizando en la implementación del backend, vemos cómo Go maneja las definiciones de GraphQL de manera única.
Definición de tipos ¶
// Struct con tags para JSON y DB
type Pokemon struct {
ID int `json:"id" db:"id"`
Name string `json:"name" db:"name"`
UpVotes int `json:"upVotes" db:"up_votes"`
}
// Tipos GraphQL
var pokemonType = graphql.NewObject(
graphql.ObjectConfig{
Name: "Pokemon",
Fields: graphql.Fields{
"id": &graphql.Field{Type: graphql.Int},
"name": &graphql.Field{Type: graphql.String},
"upVotes": &graphql.Field{Type: graphql.Int},
},
},
)
Resolvers y Queries ¶
Características principales: ¶
- Consultas flexibles:
- Lista de Pokémon
- Resultados con estadísticas
- Pares aleatorios
- SQLx para base de datos:
- Mapeo automático a structs
- Extensión ligera sobre
database/sql
Developer Experience en Backend ¶
- GraphQL Studio integrado:
- Testing de queries
- Exploración de schema
- Documentación interactiva
- Ventajas sobre REST:
- Tipo-seguro
- Auto-documentado
- Playground integrado
Colaboración Frontend-Backend ¶
El enfoque GraphQL facilita:
- Desarrollo paralelo
- Endpoints flexibles
- Queries personalizadas
Esta implementación contrasta con los enfoques monolíticos de Rails y Phoenix, ofreciendo una clara separación de responsabilidades pero requiriendo más configuración inicial.
Mutaciones y arquitectura en GraphQL ¶
Continuando con la implementación backend, vemos cómo se manejan las mutaciones y la configuración del servidor.
Mutaciones en GraphQL ¶
// Definición de mutación para votos
var mutationType = graphql.NewObject(
graphql.ObjectConfig{
Name: "Mutation",
Fields: graphql.Fields{
"vote": &graphql.Field{
Type: voteResultType,
Args: graphql.FieldConfigArgument{
"upVoteId": &graphql.ArgumentConfig{Type: graphql.Int},
"downVoteId": &graphql.ArgumentConfig{Type: graphql.Int},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
// Lógica de transacción
}
},
},
},
)
Configuración del servidor ¶
// Setup básico del servidor
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: rootQuery,
Mutation: mutationType,
})
handler := cors.Default().Handler(
&relay.Handler{Schema: &schema}
)
Comparativa de métricas ¶
- Total del proyecto:
- 952 líneas totales
- 14 archivos TypeScript
- 5 archivos Go
- División equitativa frontend/backend
Lecciones aprendidas ¶
Ventajas: ¶
- API estandarizada
- Herramientas de desarrollo robustas
- Separación clara de responsabilidades
Desventajas: ¶
- Complejidad de configuración
- Dificultad en generación de tipos
- Overhead de desarrollo inicial
Esta implementación representa un contraste significativo con los enfoques monolíticos anteriores (Rails y Phoenix), ofreciendo mayor flexibilidad pero requiriendo más configuración y coordinación entre equipos.
T3 Stack: el regreso a los orígenes ¶
Después de explorar GraphQL, llegamos a una implementación más moderna y tipo-segura con el T3 Stack.
Setup y estructura básica ¶
// Creación del proyecto
pnpm create t3-app
// Estructura básica
/pages
index.tsx
results.tsx
/server
/api
/routers
pokemon.ts
tRPC vs GraphQL ¶
Query con tRPC ¶
// Definición de query
const getPair = publicProcedure.query(async () => {
const randomIds = [
Math.floor(Math.random() * 1025),
Math.floor(Math.random() * 1025)
] as const;
const pokemon = await db.pokemon.findMany({
where: { id: { in: randomIds }}
});
return pokemon;
});
// Uso en el cliente
const { data } = api.pokemon.getPair.useQuery();
Ventajas sobre GraphQL ¶
-
Inferencia de tipos nativa:
- Sin necesidad de code-gen
- Sin problemas de nullabilidad
- Tipado automático entre cliente y servidor
-
Navegación de código:
- Command-click funcional
- Salto directo entre cliente y servidor
- Todo en el mismo ecosistema TypeScript
-
Nullabilidad simplificada:
- Sin exclamaciones extras
- Sin assertions innecesarias
- Tipado más preciso y seguro
Este enfoque representa una evolución significativa sobre la implementación GraphQL que vimos anteriormente, ofreciendo una experiencia de desarrollo más integrada y tipo-segura.
Prisma, seeding y routing en T3 ¶
Continuando con el T3 Stack, exploramos la gestión de datos y navegación.
Prisma como ORM ¶
// schema.prisma
model Pokemon {
id Int @id
name String
voteFor Vote[] @relation("VotedFor")
voteAgainst Vote[] @relation("VotedAgainst")
}
model Vote {
winner Pokemon @relation("VotedFor")
loser Pokemon @relation("VotedAgainst")
}
Características destacadas: ¶
- Schema como fuente de verdad
- Generación automática de:
- Tipos TypeScript
- Migraciones
- Cliente de base de datos
- Extensión VS Code para mejor DX
Desafíos con seeding ¶
// seed.ts
import { PrismaClient } from '@prisma/client';
import { fetchPokemonData } from './pokeapi';
const prisma = new PrismaClient();
async function main() {
const pokemonData = await fetchPokemonData();
await prisma.pokemon.createMany({
data: pokemonData
});
}
- Sin soporte nativo para seeding
- Necesidad de scripts personalizados
- Problemas con
ts-node
- Solución con
tsx
Sistema de rutas ¶
// router.tsx
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout>}>
<Route index element={<Home />} />
<Route path="results" element={<Results />} />
</Route>
</Routes>
</BrowserRouter>
Ventajas del routing basado en configuración: ¶
- Layouts compartidos fácilmente
- Fuente única de verdad para rutas
- Anidamiento intuitivo con
Outlet
Esta implementación muestra cómo T3 combina las mejores prácticas modernas de desarrollo web, aunque con algunos desafíos en áreas específicas como el seeding de datos.
Los desafíos del layout en Pages Router ¶
Después de ver las ventajas del T3 Stack, nos encontramos con uno de sus mayores desafíos: la gestión de layouts.
La complejidad de los layouts ¶
// pages/vote.tsx
export default function VotePage() {
getLayout = getLayout;
// ...
}
// utils/layout.tsx
export function getLayout(page: ReactElement) {
return <RootLayout>{page}</RootLayout>;
}
// _app.tsx
export default function App({ Component, pageProps }) {
const getLayout = Component.getLayout || ((page) => page);
return getLayout(<Component {...pageProps} />);
}
Comparativa de métricas ¶
- T3 Stack completo:
- 600 líneas totales
- 13 archivos TypeScript
- 22 archivos en total
- 450 líneas de TypeScript puro
Problemas principales ¶
Layouts ¶
- Configuración compleja
- No soporta anidamiento natural
- Requiere boilerplate extenso
- Documentación confusa
Rendimiento ¶
- Cold starts significativos
- Latencia en operaciones
- Sin caché por defecto
- Espera a conexión de base de datos
Beneficios del enfoque fullstack ¶
- Código más conciso
- Lógica unificada
- Menor superficie para bugs
- Mantenimiento simplificado
Este análisis muestra cómo, a pesar de las mejoras en DX que ofrece T3, algunos aspectos como layouts y rendimiento siguen siendo desafiantes comparados con las implementaciones que vimos en Phoenix y Rails.
React Server Components: simplicidad y rendimiento ¶
La versión final con React Server Components (RSC) muestra un enfoque radicalmente más simple y eficiente.
Server Actions y componentes ¶
// page.tsx
async function VoteContent() {
const twoPokemon = await getRandomPokemon();
async function vote(formData: FormData) {
'use server';
const pokemonId = formData.get('pokemonId');
const loser = twoPokemon.find(p => p.id !== pokemonId);
await recordBattle(pokemonId, loser.id);
revalidatePath('/');
}
return (
<div className="flex justify-center">
{twoPokemon.map(pokemon => (
<form action={vote}>
<input type="hidden" name="pokemonId" value={pokemon.id} />
<button type="submit">{pokemon.name}</button>
</form>
))}
</div>
);
}
Características destacadas ¶
- Un solo request por interacción
- Caché inteligente:
export const getAllPokemon = cache(async () => { await connection(); // Dynamic flag // Fetch and cache data }, { revalidate: 99999999 });
Ventajas principales ¶
-
Simplicidad:
- Menos código
- Lógica unificada
- Sin estado cliente
-
Rendimiento:
- Respuesta inmediata
- Caché agresivo
- Revalidación selectiva
-
Developer Experience:
- Layouts naturales
- Tipado end-to-end
- Menos boilerplate
Esta implementación representa una evolución significativa sobre las versiones anteriores, combinando la simplicidad de Phoenix LiveView con el rendimiento de una aplicación moderna.
Optimizaciones avanzadas con RSC ¶
Profundizando en las optimizaciones avanzadas del RSC, vemos soluciones elegantes para el rendimiento.
Sistema de caché y datos ¶
// Caché de Pokémon
export const getAllPokemon = cache(async () => {
// Fetch una vez y cachear permanentemente
}, { revalidate: 99999999 });
// Almacenamiento KV para votos
async function recordBattle(winner: number, loser: number) {
await kv.incr(`pokemon:${winner}:wins`);
await kv.incr(`pokemon:${loser}:losses`);
await kv.lpush('battles', { winner, loser });
}
Optimización turbo ¶
// Manejo de pares con cookies
async function VoteContent() {
// Obtener par actual de cookies
const currentPair = await cookies().get('currentPair');
const pokemon = currentPair
? JSON.parse(currentPair)
: await getRandomPokemon();
// Preparar siguiente par
const nextPair = await getRandomPokemon();
return (
<>
<div className="hidden">
{/* Preload de imágenes */}
<img src={nextPair[0].sprite} />
<img src={nextPair[1].sprite} />
</div>
<form action={async () => {
await cookies.set('currentPair', JSON.stringify(nextPair));
}}>
{/* Contenido visible */}
</form>
</>
);
}
Ventajas clave ¶
-
Caché eficiente:
- Sin necesidad de seeds
- Datos persistentes
- Revalidación selectiva
-
Optimizaciones de UX:
- Precarga de imágenes
- Estado persistente en cookies
- Transiciones instantáneas
-
Simplicidad arquitectónica:
- Sin base de datos tradicional
- KV para datos simples
- Scope automático en acciones
Conclusiones sobre RSC y comparativa final ¶
Métricas finales ¶
- RSC versión base:
- 367 líneas de código
- 12 archivos
- La implementación más concisa
Optimizaciones de caché ¶
// Componente con caché
export default function Results() {
return cache(async () => {
const rankings = await getRankings();
return (
<div>
{/* Contenido cacheado */}
</div>
);
});
}
Aspectos destacados ¶
-
Revalidación inteligente:
- Manejo selectivo de caché
- Actualización mediante cookies
- Sin invalidación innecesaria
-
Rendimiento excepcional:
- Comparable o superior a Phoenix
- Menos JavaScript en cliente
- Carga instantánea con caché
-
Developer Experience superior:
- Patrones simples y componibles
- Suspense automático
- Pre-rendering parcial
Este análisis final demuestra cómo RSC combina lo mejor de todas las implementaciones anteriores:
- La velocidad de Phoenix
- La simplicidad de Rails
- La tipo-seguridad de T3
- El desacoplamiento de GraphQL
Todo esto mientras mantiene una base de código más pequeña y una experiencia de desarrollo superior.
Tabla comparativa para seleccionar el mejor stack ¶
Esta tabla está diseñada para ayudar a tomar decisiones al elegir el stack más adecuado, considerando contexto del proyecto, tamaño del equipo, tipo de aplicación, escalabilidad, comunidad y recursos, y experiencia del equipo.
Criterio | Rails | Elixir (Phoenix) | Go con GraphQL | T3 Stack | React Server Components (RSC) |
---|---|---|---|---|---|
Contexto del proyecto | CRUD tradicional, pequeñas a medianas aplicaciones | Tiempo real, aplicaciones altamente concurrentes | API robusta y estandarizada para equipos grandes | Aplicaciones modernas full-stack | Aplicaciones enfocadas en rendimiento y renderizado del servidor |
Tamaño del equipo | Pequeños a medianos, familiarizados con MVC | Medianos, interesados en programación funcional | Grandes, con división clara entre frontend y backend | Pequeños a medianos, enfocados en TypeScript | Cualquier tamaño, ideal para equipos con experiencia en React |
Tipo de aplicación | CRUD, e-commerce, proyectos internos | Chat en tiempo real, aplicaciones colaborativas | API pública o SaaS con múltiples clientes (web y móvil) | Dashboards, aplicaciones full-stack, prototipos rápidos | Páginas de contenido dinámico, apps centradas en SEO y rendimiento |
Escalabilidad | Buena, pero puede ser compleja en proyectos grandes | Excelente, diseñado para alta concurrencia | Muy alta, eficiente en backend pero puede requerir optimización en frontend | Escalable, pero puede ser afectado por problemas de cold start en serverless | Muy alta, soporta caché y renderizado del servidor de manera eficiente |
Comunidad y recursos | Amplia comunidad y mucha documentación | Comunidad en crecimiento, más nicho pero con buenos recursos | Comunidad sólida en backend, limitada en frontend | Muy activa, muchos recursos en TypeScript y Next.js | Activa y creciente, especialmente en el ecosistema React |
Experiencia del equipo | Ideal para equipos con experiencia en Ruby | Ideal para equipos con experiencia en programación funcional | Ideal para equipos con experiencia en backend y APIs GraphQL | Ideal para equipos familiarizados con TypeScript y Next.js | Ideal para equipos con experiencia en React y enfoques modernos de servidor |
Configuración inicial | Fácil al inicio, se complica al crecer | Moderada, requiere aprender Elixir y Phoenix | Compleja, especialmente con GraphQL y la generación de tipos | Sencilla, automatizada con Create T3 App | Compleja al principio, pero muy sencilla una vez configurada |
Rendimiento | Decente, afectado por su estructura pesada | Excelente, especialmente para tiempo real | Muy bueno en backend, depende de la configuración del frontend | Bueno, pero afectado por cold starts en serverless | Excelente, aprovecha el renderizado del servidor y el caché |
Mantenimiento | Complejo en proyectos grandes, muchos archivos | Fácil de mantener, código limpio y funcional | Moderado, requiere sincronización constante entre frontend y backend | Fácil, gracias al tipado y la integración entre frontend y backend | Fácil, simplifica la lógica al minimizar el código en el cliente |
Tipado estático | No tiene tipado estático, es dinámico | Dinámico, pero con sintaxis clara y robusta | Tipado estático fuerte en backend, necesita configuración extra en frontend | Tipado estático completo en todo el stack | Tipado estático completo en TypeScript, integración fluida con datos del servidor |
Facilidad para probar | Buena, herramientas maduras para testing | Excelente, gracias al diseño funcional de Elixir | Moderada, requiere configurar mocks y herramientas para GraphQL | Excelente, integración con TypeScript facilita pruebas seguras | Muy buena, pruebas simplificadas con datos del servidor |
Recomendaciones basadas en la tabla ¶
- Rails es ideal si necesitas crear una aplicación CRUD rápida y prefieres un stack tradicional con MVC y una comunidad madura.
- Elixir (Phoenix) es la mejor opción para aplicaciones en tiempo real o aquellas que requieren alta concurrencia y rendimiento, especialmente en tiempo real.
- Go con GraphQL es adecuado para proyectos con separación clara de frontend y backend, especialmente si se necesita una API robusta y escalable para múltiples clientes.
- T3 Stack es la opción ideal para desarrolladores familiarizados con TypeScript que quieren una experiencia full-stack con tipado estático y buena integración entre frontend y backend.
- React Server Components (RSC) es perfecto para proyectos que priorizan el rendimiento y la simplicidad, utilizando renderizado del servidor para minimizar el uso de JavaScript en el cliente.
Resumen para la toma de decisión ¶
- Si tu equipo es pequeño y prefieres rapidez y simplicidad, considera Rails o T3 Stack.
- Si buscas alta concurrencia y rendimiento en tiempo real, elige Elixir (Phoenix).
- Si necesitas una API estándar y robusta para integraciones complejas, Go con GraphQL es la mejor opción.
- Si priorizas el rendimiento y un frontend moderno, utiliza React Server Components (RSC).
- Para aplicaciones full-stack modernas y tipadas, el T3 Stack ofrece una excelente experiencia de desarrollo.
Esta tabla debería ayudarte a evaluar y seleccionar la tecnología que mejor se adapta a tus necesidades y contexto de proyecto.
Recursos ¶
-
Rails: Un framework web de código abierto escrito en Ruby, enfocado en el patrón MVC (Modelo-Vista-Controlador). Es conocido por su facilidad para crear aplicaciones rápidamente gracias a su estructura y convenciones.
-
Elixir: Un lenguaje de programación funcional diseñado para aplicaciones distribuidas y concurrentes. Basado en Erlang, Elixir es conocido por su alta escalabilidad y rendimiento.
-
Phoenix LiveView: Una extensión del framework Phoenix para Elixir que permite crear interfaces interactivas y dinámicas usando WebSockets, eliminando la necesidad de JavaScript en el frontend.
-
GraphQL: Un lenguaje de consulta para APIs que permite a los clientes solicitar exactamente los datos que necesitan, facilitando el manejo de datos y la comunicación entre frontend y backend.
-
Apollo Client: Una biblioteca para consumir APIs GraphQL en aplicaciones React. Facilita la integración de datos y el manejo de estado, además de proporcionar herramientas para el desarrollo.
-
TRPC: Una biblioteca de TypeScript que permite construir APIs seguras y totalmente tipadas sin necesidad de escribir código duplicado para los tipos en frontend y backend.
-
Next.js: Un framework de React para crear aplicaciones web con capacidades de renderizado del lado del servidor (SSR), generación estática (SSG) y componentes de servidor para mejor rendimiento.
-
Prisma: Un ORM para bases de datos que simplifica el acceso a datos en aplicaciones Node.js, proporcionando un esquema tipado para garantizar consultas seguras y eficaces.
-
React Router: Una biblioteca para React que permite manejar la navegación y el enrutamiento en aplicaciones de una sola página (SPA) de manera declarativa.
-
Fly.io: Una plataforma de hosting para aplicaciones web que permite desplegar proyectos en múltiples regiones, proporcionando escalabilidad y latencia baja. Fly.io pone los servicios en reposo cuando no hay tráfico, reduciendo costos.
-
Vercel: Una plataforma de despliegue para aplicaciones web, optimizada para frameworks como Next.js. Ofrece capacidades de entrega rápida de contenido y funcionalidades de escalado automático.
-
PokéAPI: Una API pública que proporciona datos sobre Pokémon, incluyendo nombres, características, imágenes y otros detalles de la franquicia Pokémon.
-
Upstash (Vercel KV): Un servicio de almacenamiento clave-valor (KV) que se usa para almacenar datos temporalmente en aplicaciones sin necesidad de bases de datos tradicionales. Se integra bien con Vercel para caché rápido.
-
React Server Components (RSC): Una característica de React que permite cargar componentes en el servidor, mejorando el rendimiento al minimizar el JavaScript necesario en el cliente.
Estas herramientas y tecnologías forman la base de comparación del video, mostrando cómo evolucionaron los enfoques y prácticas en desarrollo web a lo largo de los años.
Remate ¶
Si bien cada stack tiene sus fortalezas, React Server Components brilla por su rendimiento y simplicidad, especialmente para aplicaciones modernas. El T3 Stack ofrece la mejor experiencia full-stack, mientras que Phoenix lidera en tiempo real y concurrencia. Rails sigue siendo sólido para aplicaciones tradicionales, y Go con GraphQL destaca en APIs robustas y escalables.
¡Elige tu arma sabiamente!
Escrito por:
Daniel Primo
12 recursos para developers cada domingo en tu bandeja de entrada
Además de una skill práctica bien explicada, trucos para mejorar tu futuro profesional y una pizquita de humor útil para el resto de la semana. Gratis.