GraphQL vs REST API - Filosofias de Diseno y Criterios de Seleccion

16 min de lectura | 2025.12.02

En el diseno de APIs, “GraphQL o REST, cual deberia elegir” es un tema frecuentemente debatido. Ambos tienen filosofias de diseno fundamentalmente diferentes, y no se trata de cual es mejor, sino de hacer la eleccion apropiada segun los requisitos del proyecto. En este articulo, buscamos comprender profundamente las diferencias entre ambos y poder tomar decisiones correctas.

Diferencias en la Filosofia de Diseno

REST: Arquitectura Orientada a Recursos

REST es una filosofia de diseno centrada en “recursos”.

flowchart TB
    subgraph REST["Principios de Diseno REST"]
        URI["/users/123<br/>URI que identifica unicamente el recurso User"]

        subgraph Methods["Los metodos HTTP expresan acciones"]
            GET["GET /users/123 → Obtener usuario"]
            PUT["PUT /users/123 → Actualizar usuario"]
            DELETE["DELETE /users/123 → Eliminar usuario"]
            POST["POST /users → Crear usuario"]
        end
    end

GraphQL: Enfoque de Lenguaje de Consulta

GraphQL es una filosofia de diseno donde “el cliente obtiene declarativamente los datos que necesita”.

flowchart TB
    subgraph GraphQL["Principios de Diseno GraphQL"]
        Endpoint["/graphql<br/>Endpoint unico"]

        subgraph Query["Especificar datos necesarios con consulta"]
            Q1["user(id: '123')"]
            Q2["→ name"]
            Q3["→ email"]
            Q4["→ posts(limit: 5)"]
            Q5["  → title"]
        end

        Endpoint --> Query
    end

Comparacion de Patrones de Obtencion de Datos

Problema de Over-fetching

// REST: Se devuelven datos innecesarios
// GET /users/123
{
  "id": "123",
  "name": "Tanaka Taro",
  "email": "tanaka@example.com",
  "phone": "090-1234-5678",        // innecesario
  "address": "Tokyo...",           // innecesario
  "createdAt": "2024-01-01",       // innecesario
  "updatedAt": "2024-12-01",       // innecesario
  "settings": { ... },             // innecesario
  "profile": { ... }               // innecesario
}

// GraphQL: Obtener solo los campos necesarios
// query { user(id: "123") { name, email } }
{
  "data": {
    "user": {
      "name": "Tanaka Taro",
      "email": "tanaka@example.com"
    }
  }
}

Problema de Under-fetching (N+1 Requests)

// REST: Se necesitan multiples solicitudes
// 1. GET /users/123
// 2. GET /users/123/posts
// 3. GET /posts/1/comments
// 4. GET /posts/2/comments
// ... (problema N+1)

// GraphQL: Obtener todos los datos en 1 solicitud
query {
  user(id: "123") {
    name
    posts {
      title
      comments {
        content
        author { name }
      }
    }
  }
}

Definicion de Esquema

REST: OpenAPI/Swagger

# openapi.yaml
openapi: 3.0.0
info:
  title: User API
  version: 1.0.0

paths:
  /users/{id}:
    get:
      summary: Obtener usuario
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Exito
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string
          format: email

GraphQL: SDL (Schema Definition Language)

# schema.graphql
type User {
  id: ID!
  name: String!
  email: String!
  posts(limit: Int, offset: Int): [Post!]!
  createdAt: DateTime!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
}

type Comment {
  id: ID!
  content: String!
  author: User!
}

type Query {
  user(id: ID!): User
  users(filter: UserFilter): [User!]!
  post(id: ID!): Post
}

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

input CreateUserInput {
  name: String!
  email: String!
}

Comparacion de Implementacion

Servidor REST (Express)

// REST API (Express + TypeScript)
import express from 'express';

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

// Obtener usuario
app.get('/users/:id', async (req, res) => {
  const user = await db.user.findUnique({
    where: { id: req.params.id }
  });

  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }

  res.json(user);
});

// Obtener posts del usuario
app.get('/users/:id/posts', async (req, res) => {
  const posts = await db.post.findMany({
    where: { authorId: req.params.id },
    take: parseInt(req.query.limit as string) || 10,
    skip: parseInt(req.query.offset as string) || 0
  });

  res.json(posts);
});

// Crear usuario
app.post('/users', async (req, res) => {
  const { name, email } = req.body;

  const user = await db.user.create({
    data: { name, email }
  });

  res.status(201).json(user);
});

Servidor GraphQL (Apollo Server)

// GraphQL API (Apollo Server + TypeScript)
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
    posts(limit: Int, offset: Int): [Post!]!
  }

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

  type Query {
    user(id: ID!): User
    users: [User!]!
  }

  type Mutation {
    createUser(name: String!, email: String!): User!
  }
`;

const resolvers = {
  Query: {
    user: async (_, { id }) => {
      return db.user.findUnique({ where: { id } });
    },
    users: async () => {
      return db.user.findMany();
    }
  },

  Mutation: {
    createUser: async (_, { name, email }) => {
      return db.user.create({ data: { name, email } });
    }
  },

  // Resolver a nivel de campo
  User: {
    posts: async (parent, { limit = 10, offset = 0 }) => {
      return db.post.findMany({
        where: { authorId: parent.id },
        take: limit,
        skip: offset
      });
    }
  }
};

const server = new ApolloServer({ typeDefs, resolvers });

Estrategias de Cache

REST: Aprovechamiento del Cache HTTP

// REST: Cache HTTP estandar
app.get('/users/:id', async (req, res) => {
  const user = await getUser(req.params.id);

  res
    .set('Cache-Control', 'public, max-age=300')  // Cache de 5 minutos
    .set('ETag', `"${user.version}"`)
    .json(user);
});

// Lado del cliente
fetch('/users/123', {
  headers: {
    'If-None-Match': '"v1"'  // Solicitud condicional
  }
});
// → 304 Not Modified (cache valido)

GraphQL: Cache del Lado del Cliente

// GraphQL: Cache normalizado de Apollo Client
import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: '/graphql',
  cache: new InMemoryCache({
    typePolicies: {
      User: {
        keyFields: ['id'],  // Clave del cache
      },
      Post: {
        keyFields: ['id'],
      }
    }
  })
});

// El cache se normaliza automaticamente
// User:123 → { name: "...", email: "..." }
// Post:456 → { title: "...", author: { __ref: "User:123" } }

Manejo de Errores

REST: Codigos de Estado HTTP

// REST: Codigos de estado HTTP estandar
app.get('/users/:id', async (req, res) => {
  try {
    const user = await getUser(req.params.id);

    if (!user) {
      return res.status(404).json({
        error: 'NOT_FOUND',
        message: 'Usuario no encontrado'
      });
    }

    res.json(user);
  } catch (error) {
    res.status(500).json({
      error: 'INTERNAL_ERROR',
      message: 'Ocurrio un error en el servidor'
    });
  }
});

// Ejemplo de respuesta
// HTTP 404
{
  "error": "NOT_FOUND",
  "message": "Usuario no encontrado"
}

GraphQL: Array de Errors

// GraphQL: Posibles errores parciales
const resolvers = {
  Query: {
    user: async (_, { id }) => {
      const user = await getUser(id);
      if (!user) {
        throw new GraphQLError('Usuario no encontrado', {
          extensions: {
            code: 'NOT_FOUND',
            argumentName: 'id'
          }
        });
      }
      return user;
    }
  }
};

// Ejemplo de respuesta (exito parcial)
{
  "data": {
    "user": null,
    "posts": [...]  // Otros campos exitosos
  },
  "errors": [
    {
      "message": "Usuario no encontrado",
      "path": ["user"],
      "extensions": {
        "code": "NOT_FOUND"
      }
    }
  ]
}

Caracteristicas de Rendimiento

Tabla Comparativa

AspectoRESTGraphQL
Numero de solicitudesTiende a ser mayorSe puede minimizar
Tamano del payloadOver-fetchingOptimizado
Cache HTTPSoporte nativoRequiere implementacion adicional
Cache CDNFacilRequiere ingenio
Problema N+1 (servidor)No existeSolucion con DataLoader

Solucion al Problema N+1 en GraphQL

// Optimizacion con DataLoader
import DataLoader from 'dataloader';

// Definicion del batch loader
const userLoader = new DataLoader(async (userIds: string[]) => {
  const users = await db.user.findMany({
    where: { id: { in: userIds } }
  });

  // Retornar manteniendo el orden de IDs
  const userMap = new Map(users.map(u => [u.id, u]));
  return userIds.map(id => userMap.get(id));
});

// Uso en resolver
const resolvers = {
  Post: {
    author: (parent, _, context) => {
      return context.userLoader.load(parent.authorId);
    }
  }
};

// Resultado: Consulta batch en lugar de consultas individuales
// SELECT * FROM users WHERE id IN ('1', '2', '3', ...)

Consideraciones de Seguridad

REST: Control por Endpoint

// REST: Autorizacion por ruta
app.get('/admin/users', requireAdmin, async (req, res) => {
  // Solo accesible para administradores
});

app.get('/users/:id', async (req, res) => {
  // Endpoint publico
});

GraphQL: Control a Nivel de Campo

// GraphQL: Control mediante directivas
const typeDefs = `#graphql
  directive @auth(requires: Role!) on FIELD_DEFINITION

  type User {
    id: ID!
    name: String!
    email: String! @auth(requires: OWNER)
    salary: Int @auth(requires: ADMIN)
  }
`;

// Limitacion de complejidad de consultas
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    createComplexityLimitRule(1000)  // Limite de complejidad
  ]
});

Medidas de Seguridad Especificas de GraphQL

// 1. Limitacion de profundidad de consulta
import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
  validationRules: [depthLimit(10)]  // Profundidad maxima 10
});

// 2. Desactivar introspeccion (produccion)
const server = new ApolloServer({
  introspection: process.env.NODE_ENV !== 'production'
});

// 3. Consultas persistidas
const server = new ApolloServer({
  persistedQueries: {
    cache: new RedisCache()
  }
});

Criterios de Seleccion

Casos donde REST es Apropiado

Situaciones para elegir REST:

✓ Operaciones CRUD simples como centro
✓ Desea aprovechar al maximo el cache HTTP
✓ Cache CDN/edge es importante
✓ El equipo tiene amplia experiencia en REST
✓ Proporcionado como API publica
✓ Muchas cargas/descargas de archivos

Casos donde GraphQL es Apropiado

Situaciones para elegir GraphQL:

✓ Requisitos de datos complejos (muchas relaciones)
✓ Multiples clientes (Web, Mobile, etc.)
✓ Desarrollo liderado por el cliente (frontend primero)
✓ Funciones en tiempo real (Subscriptions)
✓ BFF de microservicios (Backend for Frontend)
✓ Optimizacion de ancho de banda importante (movil)

Enfoque Hibrido

// Patron que usa ambos
const app = express();

// REST: Carga de archivos
app.post('/upload', upload.single('file'), (req, res) => {
  res.json({ url: req.file.path });
});

// REST: Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

// GraphQL: Obtencion/actualizacion de datos
app.use('/graphql', apolloMiddleware);

Resumen

GraphQL y REST son paradigmas de diseno de API con diferentes fortalezas.

Fortalezas de REST

  • Aprovecha al maximo las funciones HTTP
  • Simple y facil de entender
  • Rico ecosistema de herramientas
  • Cache facil

Fortalezas de GraphQL

  • Obtencion de datos flexible
  • Esquema con tipado seguro
  • Obtencion de datos complejos en una sola solicitud
  • Mejora la experiencia del desarrollador

Guia de Seleccion

  1. Analizar requisitos: Complejidad de datos, diversidad de clientes
  2. Habilidades del equipo: Experiencia existente y costo de aprendizaje
  3. Requisitos de rendimiento: Cache, ancho de banda, latencia
  4. Extensibilidad futura: Escalabilidad, mantenibilidad

Cualquiera que elija, puede construir una API excelente si disena e implementa adecuadamente. Lo importante es hacer la eleccion que se ajuste a las caracteristicas del proyecto.

Enlaces de Referencia

← Volver a la lista