Vista detallada de codigo de programacion en un tema oscuro en la pantalla de una computadora.

REST API vs GraphQL vs tRPC en 2026: Cual deberia usar?

📷 Stanislav Kondratiev / Pexels

REST API vs GraphQL vs tRPC en 2026: Cual deberia usar?

Una comparacion detallada de REST, GraphQL y tRPC para desarrollo web moderno. Conozca los pros, contras, caracteristicas de rendimiento y mejores casos de uso para cada enfoque.

19 de marzo de 202614 min de lectura

Elegir como su frontend se comunica con su backend es una de las decisiones arquitectonicas mas importantes en un proyecto web. En 2026, los desarrolladores tienen tres opciones maduras y probadas en batalla: REST, GraphQL y tRPC. Cada una tiene fortalezas distintas, compromisos y casos de uso ideales. Este articulo proporciona una comparacion exhaustiva para ayudarle a tomar la decision correcta para su proximo proyecto.

REST API: El estandar establecido

REST (Representational State Transfer) ha sido el paradigma de API dominante durante mas de dos decadas. Mapea metodos HTTP a operaciones CRUD sobre recursos, haciendolo intuitivo y comprendido por practicamente todos los desarrolladores.

Como funciona REST

Las REST APIs exponen recursos como URLs. Los clientes interactuan con ellos usando metodos HTTP estandar:

// GET una lista de usuarios
const response = await fetch('https://api.example.com/users');
const users = await response.json();

// GET un usuario individual
const user = await fetch('https://api.example.com/users/42');

// POST para crear un nuevo usuario
const newUser = await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: 'Alice Johnson',
    email: 'alice@example.com',
  }),
});

// PUT para actualizar un usuario
await fetch('https://api.example.com/users/42', {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Alice Smith' }),
});

// DELETE un usuario
await fetch('https://api.example.com/users/42', {
  method: 'DELETE',
});

Ejemplo REST del lado del servidor (Express)

import express from 'express';

const app = express();
app.use(express.json());

// GET /users
app.get('/users', async (req, res) => {
  const { page = 1, limit = 20, sort = 'name' } = req.query;
  const users = await db.users.findMany({
    skip: (Number(page) - 1) * Number(limit),
    take: Number(limit),
    orderBy: { [sort as string]: 'asc' },
  });
  res.json({ data: users, page: Number(page), limit: Number(limit) });
});

// GET /users/:id
app.get('/users/:id', async (req, res) => {
  const user = await db.users.findUnique({
    where: { id: Number(req.params.id) },
    include: { posts: true, profile: true },
  });
  if (!user) return res.status(404).json({ error: 'User not found' });
  res.json(user);
});

// POST /users
app.post('/users', async (req, res) => {
  const user = await db.users.create({ data: req.body });
  res.status(201).json(user);
});

Fortalezas de REST

  • Universalmente comprendido: Todos los desarrolladores, todos los lenguajes, todas las plataformas soportan REST.
  • Caching HTTP: Las solicitudes GET pueden ser almacenadas en cache por navegadores, CDNs y proxies de forma inmediata.
  • Sin estado y simple: Cada solicitud contiene toda la informacion que el servidor necesita.
  • Herramientas maduras: Swagger/OpenAPI, Postman e innumerables bibliotecas.
  • Los codigos de estado son significativos: 200, 201, 404, 422, 500 transmiten informacion especifica.

Debilidades de REST

  • Over-fetching: Un endpoint devuelve todos los campos, incluso si el cliente solo necesita unos pocos.
  • Under-fetching: Obtener datos relacionados a menudo requiere multiples viajes de ida y vuelta.
  • Sin seguridad de tipos incorporada: Sin herramientas adicionales como generacion de codigo OpenAPI, no hay un contrato entre cliente y servidor.
  • Desafios de versionamiento: Los cambios incompatibles a menudo requieren endpoints /v2/ o versionamiento basado en headers.

GraphQL: Consultas flexibles

GraphQL, creado por Facebook y publicado como codigo abierto en 2015, permite al cliente especificar exactamente que datos necesita en una sola solicitud. Usa un esquema tipado para definir la superficie de la API.

Como funciona GraphQL

// Una consulta GraphQL
const query = `
  query GetUser($id: ID!) {
    user(id: $id) {
      name
      email
      posts(first: 5) {
        title
        createdAt
      }
      followers {
        totalCount
      }
    }
  }
`;

const response = await fetch('https://api.example.com/graphql', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    query,
    variables: { id: '42' },
  }),
});

const { data } = await response.json();
// data.user.name, data.user.posts, data.user.followers.totalCount

Esquema GraphQL del lado del servidor

// schema.ts (usando un enfoque schema-first)
const typeDefs = `
  type User {
    id: ID!
    name: String!
    email: String!
    posts(first: Int, after: String): PostConnection!
    followers: FollowerCount!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    createdAt: DateTime!
  }

  type PostConnection {
    edges: [PostEdge!]!
    pageInfo: PageInfo!
  }

  type Query {
    user(id: ID!): User
    users(first: Int, after: String): UserConnection!
    post(id: ID!): Post
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: ID!, input: UpdateUserInput!): User!
    deleteUser(id: ID!): Boolean!
  }

  type Subscription {
    postCreated: Post!
  }
`;

Resolvers

const resolvers = {
  Query: {
    user: async (_: unknown, { id }: { id: string }, ctx: Context) => {
      return ctx.db.users.findUnique({ where: { id } });
    },
    users: async (_: unknown, args: PaginationArgs, ctx: Context) => {
      return paginateUsers(ctx.db, args);
    },
  },
  User: {
    posts: async (parent: User, args: PaginationArgs, ctx: Context) => {
      return ctx.loaders.userPosts.load({ userId: parent.id, ...args });
    },
    followers: async (parent: User, _: unknown, ctx: Context) => {
      const count = await ctx.db.followers.count({
        where: { followeeId: parent.id },
      });
      return { totalCount: count };
    },
  },
  Mutation: {
    createUser: async (_: unknown, { input }: { input: CreateUserInput }, ctx: Context) => {
      return ctx.db.users.create({ data: input });
    },
  },
};

Fortalezas de GraphQL

  • Sin over-fetching ni under-fetching: Los clientes solicitan exactamente los campos que necesitan.
  • Un unico endpoint: Todas las operaciones pasan por una URL.
  • Esquema fuertemente tipado: El esquema sirve como documentacion viva y habilita herramientas poderosas.
  • Introspeccion: Los clientes pueden descubrir toda la superficie de la API programaticamente.
  • Subscriptions: Soporte incorporado para datos en tiempo real via WebSockets.
  • Ideal para clientes diversos: Una aplicacion movil puede solicitar campos minimos mientras una aplicacion de escritorio obtiene todo.

Debilidades de GraphQL

  • Complejidad: Resolvers, data loaders, persisted queries y gestion de esquemas anaden carga cognitiva.
  • El caching es mas dificil: Ya que todas las solicitudes son POSTs a un unico endpoint, el caching HTTP tradicional no funciona. Necesita caching del lado del cliente (Apollo, urql) o persisted queries.
  • Problema de consultas N+1: Sin data loaders, los resolvers anidados pueden disparar consultas excesivas a la base de datos.
  • Subida de archivos: No soportada nativamente; requiere especificaciones de solicitud multipart o endpoints de subida separados.
  • Superficie de seguridad: Consultas profundamente anidadas o costosas pueden causar denegacion de servicio a menos que implemente limitacion de profundidad de consulta y analisis de complejidad.

tRPC: Seguridad de tipos de extremo a extremo

tRPC es un framework especifico de TypeScript que le permite llamar funciones del servidor directamente desde el cliente con seguridad de tipos completa y sin generacion de codigo. Esta disenado para proyectos monorepo donde tanto el frontend como el backend estan escritos en TypeScript.

Como funciona tRPC

tRPC elimina la capa de API como un concepto separado. Usted define procedimientos en el servidor, y el cliente los llama como si fueran funciones locales.

Definicion del router del lado del servidor:

// server/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';

export const userRouter = router({
  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input, ctx }) => {
      const user = await ctx.db.users.findUnique({
        where: { id: input.id },
        include: { posts: true, profile: true },
      });
      if (!user) throw new TRPCError({ code: 'NOT_FOUND' });
      return user;
    }),

  list: publicProcedure
    .input(z.object({
      page: z.number().default(1),
      limit: z.number().min(1).max(100).default(20),
      search: z.string().optional(),
    }))
    .query(async ({ input, ctx }) => {
      const { page, limit, search } = input;
      const where = search
        ? { name: { contains: search, mode: 'insensitive' as const } }
        : {};
      const [users, total] = await Promise.all([
        ctx.db.users.findMany({
          where,
          skip: (page - 1) * limit,
          take: limit,
        }),
        ctx.db.users.count({ where }),
      ]);
      return { users, total, page, limit };
    }),

  create: protectedProcedure
    .input(z.object({
      name: z.string().min(1).max(100),
      email: z.string().email(),
    }))
    .mutation(async ({ input, ctx }) => {
      return ctx.db.users.create({ data: input });
    }),

  update: protectedProcedure
    .input(z.object({
      id: z.string(),
      name: z.string().min(1).max(100).optional(),
      email: z.string().email().optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      const { id, ...data } = input;
      return ctx.db.users.update({ where: { id }, data });
    }),
});

Uso del lado del cliente (con React):

// Cliente: seguridad de tipos completa, sin generacion de codigo necesaria
import { trpc } from '../utils/trpc';

function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading, error } = trpc.user.getById.useQuery({ id: userId });

  const updateUser = trpc.user.update.useMutation({
    onSuccess: () => {
      // Invalidar y volver a obtener
      utils.user.getById.invalidate({ id: userId });
    },
  });

  if (isLoading) return <Skeleton />;
  if (error) return <ErrorMessage error={error.message} />;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <button onClick={() => updateUser.mutate({ id: userId, name: 'New Name' })}>
        Actualizar nombre
      </button>
    </div>
  );
}

Cada entrada, salida y error esta completamente tipado. Si cambia el nombre de un campo en el servidor, el cliente mostrara un error de TypeScript instantaneamente.

Fortalezas de tRPC

  • Seguridad de tipos de extremo a extremo: Los cambios en el servidor se reflejan inmediatamente en los tipos del cliente sin generacion de codigo.
  • Boilerplate minimo: Sin archivos de esquema, sin definiciones de rutas de API, sin paso de generacion de cliente.
  • Excelente experiencia de desarrollo: Autocompletado, documentacion en linea y soporte de refactorizacion en su IDE.
  • Validacion de entrada incorporada: Los esquemas Zod validan las entradas en tiempo de ejecucion y proporcionan tipos en tiempo de compilacion.
  • Integracion con React Query: Soporte de primera clase para caching, actualizaciones optimistas y refetching en tiempo real.
  • Subscriptions: Soporte de WebSocket para funcionalidades en tiempo real.

Debilidades de tRPC

  • Solo TypeScript: Tanto el cliente como el servidor deben ser TypeScript. No funciona con Python, Go u otros lenguajes de backend.
  • Centrado en monorepo: Funciona mejor cuando el cliente y el servidor comparten una base de codigo. Menos practico cuando los equipos trabajan en repositorios separados.
  • Sin caso de uso de API publica: No adecuado para APIs consumidas por terceros ya que no hay un esquema independiente del lenguaje.
  • Cliente y servidor acoplados: El acoplamiento estrecho puede convertirse en una preocupacion de mantenimiento a medida que los equipos crecen.
  • Ecosistema mas pequeno: Menos tutoriales, herramientas y recursos comunitarios comparado con REST o GraphQL.

Tabla comparativa

CaracteristicaRESTGraphQLtRPC
Seguridad de tiposManual (OpenAPI codegen)Basada en esquemaAutomatica (TypeScript)
Over-fetchingComunEliminadoMinimo (retornos tipados)
Under-fetchingComun (N+1 solicitudes)EliminadoMinimo
CachingExcelente (HTTP nativo)Complejo (lado del cliente)React Query incorporado
Curva de aprendizajeBajaMedia-AltaBaja (si conoce TS)
Madurez del toolingExcelenteMuy buenaBuena
Soporte de lenguajesCualquieraCualquieraSolo TypeScript
Tiempo realWebSockets/SSE (separado)SubscriptionsSubscriptions
Subida de archivosNativoRequiere solucion alternativaRequiere solucion alternativa
APIs publicasIdealBuenoNo adecuado
Generacion de codigoOpcional (OpenAPI)A menudo necesariaNo necesaria
RendimientoBuenoBueno (con batching)Excelente (overhead minimo)
Documentacion de APISwagger/OpenAPIIntrospeccion de esquemaTipos TypeScript

Cuando usar cada uno

Elija REST cuando

  • Este construyendo una API publica consumida por terceros en varios lenguajes.
  • Necesite caching HTTP a nivel de CDN o navegador (por ejemplo, un sitio con mucho contenido).
  • Su equipo incluya desarrolladores mas familiarizados con APIs HTTP tradicionales.
  • Este construyendo microservicios que se comunican entre si a traves de diferentes runtimes de lenguajes.
  • Desee la arquitectura mas simple posible para una aplicacion CRUD sencilla.

Elija GraphQL cuando

  • Tenga multiples clientes (web, movil, TV, reloj) que necesitan diferentes formas de datos de la misma API.
  • Sus datos sean profundamente relacionales y los clientes frecuentemente necesiten datos anidados en una sola solicitud.
  • Necesite funcionalidades en tiempo real (subscriptions) estrechamente integradas con su capa de consulta.
  • Desee una API autodocumentada con introspeccion para herramientas de desarrollo.
  • Este construyendo un gateway que agrega datos de multiples servicios backend.

Elija tRPC cuando

  • Todo su stack sea TypeScript (tanto frontend como backend).
  • Trabaje en un monorepo donde el codigo del cliente y servidor viven juntos.
  • Valore la velocidad de desarrollo y desee la velocidad de iteracion mas rapida posible.
  • Este construyendo una herramienta interna o producto SaaS donde la API no se expone publicamente.
  • La seguridad de tipos sea una prioridad maxima y desee detectar discrepancias de API en tiempo de compilacion.

Patrones de arquitectura del mundo real

Enfoque hibrido: tRPC + REST

Muchos equipos usan tRPC para su comunicacion interna frontend-a-backend y exponen una REST API separada para integraciones de terceros:

// Router tRPC interno para el dashboard
export const appRouter = router({
  user: userRouter,
  billing: billingRouter,
  analytics: analyticsRouter,
});

// REST API publica para integraciones
app.get('/api/v1/users/:id', async (req, res) => {
  const user = await db.users.findUnique({
    where: { id: req.params.id },
    select: publicUserFields,
  });
  res.json(user);
});

GraphQL como Gateway

GraphQL funciona bien como un gateway de API que federa datos de multiples microservicios:

// Esquema gateway que combina multiples servicios
const gatewaySchema = stitchSchemas({
  subschemas: [
    { schema: userServiceSchema, executor: userServiceExecutor },
    { schema: orderServiceSchema, executor: orderServiceExecutor },
    { schema: inventoryServiceSchema, executor: inventoryServiceExecutor },
  ],
});

REST con OpenAPI para seguridad de tipos

Si elige REST pero desea seguridad de tipos, la generacion de codigo OpenAPI cierra la brecha:

# openapi.yaml
paths:
  /users/{id}:
    get:
      operationId: getUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
# Generar cliente tipado
npx openapi-typescript-codegen --input openapi.yaml --output src/api
// Cliente generado con seguridad de tipos completa
import { UserService } from './api';

const user = await UserService.getUser({ id: '42' });
// user esta completamente tipado

Consideraciones de rendimiento

Rendimiento de REST

REST se beneficia del multiplexing HTTP/2 y el caching del navegador. Para APIs con muchas lecturas que sirven contenido cacheable, REST puede ser extremadamente rapido. La desventaja son los multiples viajes de ida y vuelta cuando necesita datos de varios endpoints.

Rendimiento de GraphQL

GraphQL reduce los viajes de ida y vuelta al permitir consultas complejas en una sola solicitud. Sin embargo, el servidor debe resolver arboles de consultas potencialmente complejos. Sin optimizacion adecuada (data loaders, limites de complejidad de consulta, persisted queries), GraphQL puede ser en realidad mas lento que REST.

Rendimiento de tRPC

tRPC tiene un overhead de serializacion minimo ya que se comunica usando JSON simple sobre HTTP. Con el batching de solicitudes habilitado, multiples llamadas de procedimiento se combinan en una sola solicitud HTTP, reduciendo los viajes de ida y vuelta. La integracion con React Query proporciona caching del lado del cliente que reduce aun mas las solicitudes de red.

Conclusion

No hay un unico mejor enfoque de API. La eleccion correcta depende de su equipo, su stack tecnologico y los requisitos de su proyecto.

REST sigue siendo la opcion predeterminada para APIs publicas, comunicacion entre microservicios y equipos que trabajan con multiples lenguajes. Su simplicidad y soporte universal lo convierten en la opcion segura para la mayoria de los escenarios.

GraphQL sobresale cuando tiene datos complejos y relacionales y multiples aplicaciones cliente que necesitan diferentes vistas de esos datos. Agrega complejidad, pero la flexibilidad que proporciona puede valer la pena para el caso de uso correcto.

tRPC es la mejor opcion para proyectos monorepo en TypeScript donde la velocidad de desarrollo y la seguridad de tipos son prioridades. Elimina toda una categoria de bugs (discrepancias en contratos de API) y proporciona la experiencia de desarrollo mas rapida de las tres opciones.

Muchos equipos exitosos usan una combinacion de estos enfoques, aprovechando cada uno donde tiene mas sentido. Lo importante es comprender los compromisos y tomar una decision intencional en lugar de usar por defecto lo que uso su ultimo proyecto.

Publicaciones relacionadas