Newsletter para devsEntra

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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 ;).

Leer más

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

Interfaz de desarrollo mostrando errores y navegación en Rails

Siguiendo con los desafíos encontrados al trabajar con Rails en 2024, la experiencia de desarrollo presenta varios puntos de fricción:

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

Editor mostrando código de migración en 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

Panel de despliegue de Fly.io mostrando configuración de Rails

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

Interfaz mostrando la implementación de LiveView en Phoenix

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

Estructura de archivos y código 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

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

Código Elixir mostrando pipes y pattern matching

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
  • 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

Código de LiveView mostrando gestión de estado y actualizaciones

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:
    1. Renderizado inicial en servidor
    2. 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

Código de optimización y rutas 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

Interfaz de GraphQL Explorer y código Apollo

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 y useMutation
    • 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

Código mostrando problemas de tipado con GraphQL

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
  • 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

Definiciones de tipos y queries en Go con GraphQL

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

Implementación de mutaciones y configuración del servidor 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

Código del T3 Stack mostrando tRPC y tipos

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

Schema de Prisma y configuración de rutas

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

Configuración de layouts en Next.js 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

Implementación con React Server Components

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

  1. Simplicidad:

    • Menos código
    • Lógica unificada
    • Sin estado cliente
  2. Rendimiento:

    • Respuesta inmediata
    • Caché agresivo
    • Revalidación selectiva
  3. 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

Código de optimizaciones y manejo de estado

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

  1. Caché eficiente:

    • Sin necesidad de seeds
    • Datos persistentes
    • Revalidación selectiva
  2. Optimizaciones de UX:

    • Precarga de imágenes
    • Estado persistente en cookies
    • Transiciones instantáneas
  3. Simplicidad arquitectónica:

    • Sin base de datos tradicional
    • KV para datos simples
    • Scope automático en acciones

Conclusiones sobre RSC y comparativa final

Dashboard final mostrando estadísticas de código

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

  1. Revalidación inteligente:

    • Manejo selectivo de caché
    • Actualización mediante cookies
    • Sin invalidación innecesaria
  2. Rendimiento excepcional:

    • Comparable o superior a Phoenix
    • Menos JavaScript en cliente
    • Carga instantánea con caché
  3. 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:

Imagen de Daniel Primo

Daniel Primo

CEO en pantuflas de Web Reactiva. Programador y formador en tecnologías que cambian el mundo y a las personas. Activo en linkedin, en substack y canal @webreactiva en telegram

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.