1. Introducción
Si has seguido esta serie, te has convertido en un experto construyendo APIs REST robustas. Sabes cómo estructurar endpoints, asegurar rutas, conectar bases de datos y escribir pruebas. Pero en el mundo del desarrollo, siempre hay nuevos horizontes que explorar.
Aunque REST es increíblemente poderoso y ha sido el estándar durante años, a medida que las aplicaciones se vuelven más complejas, pueden surgir ciertos “dolores de cabeza”:
- Over-fetching (Exceso de datos): Imagina que solo necesitas los títulos de las tareas para un menú desplegable. Con REST, tu endpoint
GET /tasks
probablemente te devolverá toda la información de cada tarea: título, descripción, estado, fecha de creación, etc. Estás recibiendo y transfiriendo muchos más datos de los que realmente necesitas. - Under-fetching (Falta de datos): Ahora imagina que quieres mostrar una tarea y, al lado, el nombre del usuario que la creó. Con REST, esto generalmente requiere dos peticiones: primero,
GET /tasks/:id
para obtener la tarea (que incluye eluserId
), y luego una segunda petición,GET /users/:userId
, para obtener los detalles de ese usuario. Múltiples viajes de ida y vuelta a la red hacen la aplicación más lenta.
Aquí es donde GraphQL entra en escena como una solución elegante. GraphQL no es un framework ni una librería, es un lenguaje de consulta para APIs. Su filosofía es simple: darle el poder al cliente. En lugar de tener múltiples endpoints que devuelven estructuras de datos fijas, GraphQL expone un único endpoint que permite al cliente pedir exactamente los datos que necesita, en la forma que los necesita, y obtenerlos todos en una sola petición.
¿Y por qué NestJS? Porque su integración con GraphQL es, en una palabra, mágica. Usaremos una estrategia llamada Code-First, donde no tendremos que escribir complejos archivos de esquema (.graphql
) por separado. En su lugar, usaremos los decoradores de TypeScript que ya conocemos y amamos para definir nuestro esquema de GraphQL directamente en nuestro código.
Nuestro objetivo: En esta guía, no reemplazaremos nuestra API REST. La aumentaremos. Construiremos un endpoint de GraphQL en nuestra aplicación existente que te permitirá hacer consultas tan simples como pedir solo los títulos de tus tareas, o tan complejas como pedir una tarea, el nombre de su usuario y todas sus etiquetas asociadas, ¡todo en una sola y eficiente petición!
2. Preparando el Terreno: Instalación y Configuración
Añadir GraphQL a un proyecto NestJS existente es un proceso sorprendentemente sencillo gracias a su sistema de módulos.
Paso 1: Instalación de Dependencias 📦
Primero, necesitamos instalar el conjunto de paquetes que permiten la integración de NestJS con Apollo Server, una de las implementaciones de GraphQL más populares y robustas.
Abre tu terminal en la raíz del proyecto y ejecuta el siguiente comando:
Bash
npm install @nestjs/graphql @nestjs/apollo apollo-server-express graphql
@nestjs/graphql
: El paquete principal para la integración de GraphQL en NestJS.@nestjs/apollo
: Un envoltorio específico para usar Apollo Server.apollo-server-express
: La implementación de Apollo Server que se ejecuta sobre Express.graphql
: La librería central de JavaScript para GraphQL.
Paso 2: Configurando el GraphQLModule
Ahora, necesitamos registrar el módulo de GraphQL en nuestra aplicación, similar a como hicimos con TypeOrmModule
. Lo haremos en nuestro módulo raíz, app.module.ts
.
TypeScript
// src/app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql'; // 1. Importar
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; // 2. Importar el Driver
import { join } from 'path';
// ... otros imports
@Module({
imports: [
// ... TypeOrmModule, TasksModule, AuthModule, etc.
// 3. Configurar GraphQLModule
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'), // Estrategia Code-First
sortSchema: true,
playground: true, // Habilitar el Playground en producción sería false
}),
],
controllers: [],
providers: [],
})
export class AppModule {}
Desglosemos estas opciones:
driver: ApolloDriver
: Le dice a NestJS que estamos usando Apollo como nuestro servidor de GraphQL.autoSchemaFile
: Esta es la clave de la estrategia Code-First. En lugar de escribir un archivo de esquema a mano, le decimos a NestJS: “revisa todo mi código, busca los decoradores de GraphQL y genera automáticamente un archivo de esquema (schema.gql
) por mí”. Es un ahorro de tiempo y una fuente de verdad única.playground: true
: Esta opción habilita una herramienta web increíblemente útil llamada GraphQL Playground, a la que accederemos en un momento.
Paso 3: Lanzando el GraphQL Playground 🎮
Reinicia tu aplicación (npm run start:dev
). Si todo se instaló correctamente, no verás ningún cambio en http://localhost:3000
.
Ahora, abre tu navegador y ve a http://localhost:3000/graphql
.
¡Bienvenido al GraphQL Playground! Esta será tu herramienta principal para interactuar y probar tu API de GraphQL. Es como Postman o Insomnia, pero diseñado específicamente para GraphQL. Te ofrece autocompletado, documentación de tu esquema en tiempo real y una interfaz limpia para escribir y ejecutar consultas (queries
) y mutaciones (mutations
).
Por ahora, está vacío porque aún no hemos definido ninguna operación. Pero el hecho de que se cargue confirma que nuestra configuración es correcta.
3. Los Pilares de GraphQL en NestJS
Para trabajar con GraphQL en NestJS, necesitamos entender cuatro conceptos fundamentales. La buena noticia es que, si has seguido esta serie, ya conoces sus equivalentes en el mundo REST, lo que hace la transición mucho más intuitiva.
Pensemos en nuestra API como un restaurante de datos.
Resolvers (Resolutores): Los Chefs Especializados 👨🍳
En REST, tenemos Controladores que actúan como camareros para rutas específicas (/tasks
, /users
). En GraphQL, tenemos Resolvers.
Un Resolver es como un chef especializado en una sección del menú. No hay un chef para “platos de pollo” y otro para “platos de pescado”. En su lugar, hay un TasksResolver
que sabe cómo “resolver” (obtener, crear, modificar) todo lo relacionado con las Tareas. Es una clase que agrupa toda la lógica para una entidad específica de nuestro grafo de datos.
Queries (Consultas): Pedir del Menú 📖
En REST, para leer datos, usamos peticiones GET
. En GraphQL, usamos Queries.
Una Query es una función dentro de un Resolver que define una forma de leer datos. Es como una entrada en el menú. Podríamos tener una Query llamada tasks
para obtener todas las tareas, y otra llamada task
(con un ID como argumento) para obtener una sola. La gran diferencia es que el cliente puede especificar qué “ingredientes” (campos) de la tarea quiere recibir.
Mutations (Mutaciones): Modificar la Cocina ✍️
En REST, para escribir datos, usamos POST
, PUT
, PATCH
, y DELETE
. GraphQL simplifica esto con un solo concepto: Mutations.
Una Mutation es una función dentro de un Resolver que define una forma de modificar datos (crear, actualizar o borrar). A pesar del nombre, es un concepto muy simple. Tendríamos una Mutation llamada createTask
, otra updateTask
, y así sucesivamente. Por convención, todas las operaciones que alteran los datos se definen como Mutations.
Object Types (Tipos de Objeto): La Receta del Plato 📜
En REST, usamos DTOs y Entidades para definir la estructura de nuestros datos. En GraphQL, usamos Object Types.
Un Object Type le dice a GraphQL cuál es la “forma” de un dato. Nuestro TaskType
, por ejemplo, definirá que una Tarea tiene un id
(de tipo ID), un title
(de tipo String), un status
(de tipo String), etc. Estos tipos son los que forman el esquema, que actúa como un contrato estricto entre el cliente y el servidor, garantizando que los datos siempre tendrán la estructura esperada.
Con estos cuatro pilares, NestJS nos da todo lo que necesitamos para construir una API de GraphQL robusta y bien estructurada, utilizando decoradores que nos resultarán muy familiares.
4. Manos a la Obra: Construyendo Nuestro Primer Resolver
Vamos a exponer nuestra entidad Task
a través of GraphQL. Crearemos un resolver que permitirá a los clientes consultar tareas y crear nuevas.
Paso 1: Definiendo el Tipo de Objeto TaskType
Primero, necesitamos enseñarle a GraphQL cómo es una “Tarea”. Crearemos una clase TaskType
que servirá como nuestro Object Type. Puedes crear un nuevo archivo src/tasks/dto/task.type.ts
o definirlo en el mismo resolver. Por claridad, lo crearemos por separado.
TypeScript
// src/tasks/dto/task.type.ts
import { ObjectType, Field, ID } from '@nestjs/graphql';
import { TaskStatus } from '../entities/task.entity';
@ObjectType('Task') // El nombre que tendrá en el esquema GraphQL
export class TaskType {
@Field(() => ID) // GraphQL usa un tipo ID especial para identificadores únicos
id: string;
@Field()
title: string;
@Field()
description: string;
@Field()
status: TaskStatus;
}
Los decoradores @ObjectType
y @Field
le indican a NestJS cómo construir el esquema.
Paso 2: Creando el TasksResolver
Ahora, crearemos el “chef especializado” para nuestras tareas. Usaremos el CLI:
Bash
nest generate resolver tasks
NestJS te preguntará qué tipo de resolver quieres, elige GraphQL (code first). Esto creará el archivo src/tasks/tasks.resolver.ts
y lo registrará en tasks.module.ts
.
Paso 3: Implementando la Primera Query
Vamos a crear una query llamada tasks
que devuelva todas las tareas del usuario autenticado.
TypeScript
// src/tasks/tasks.resolver.ts
import { Resolver, Query } from '@nestjs/graphql';
import { TasksService } from './tasks.service';
import { TaskType } from './dto/task.type';
import { UseGuards } from '@nestjs/common';
import { GqlAuthGuard } from '../auth/gql-auth.guard'; // Crearemos este guard
import { GetUser } from '../auth/get-user.decorator';
import { User } from '../auth/entities/user.entity';
@Resolver(() => TaskType)
export class TasksResolver {
constructor(private tasksService: TasksService) {}
@Query(() => [TaskType]) // Define que esta query devuelve un array de TaskType
@UseGuards(GqlAuthGuard) // ¡Protegemos el endpoint!
async tasks(@GetUser() user: User) {
return this.tasksService.getAllTasks(user);
}
}
¡Un momento! ¿Qué es GqlAuthGuard
? El <a href="https://creapolis.dev/nestjs-autenticacion-y-autorizacion-desde-cero-jwt/" data-type="post" data-id="5129">AuthGuard</a>('jwt')
que usamos en REST está diseñado para peticiones HTTP estándar. Para GraphQL, necesitamos una versión ligeramente modificada que sepa cómo extraer el usuario del contexto de GraphQL.
Crea el archivo src/auth/gql-auth.guard.ts
:
TypeScript
// src/auth/gql-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
Paso 4: Implementando la Primera Mutation
Ahora, implementemos una mutación para crear una tarea. Necesitamos un DTO de entrada, que en GraphQL se llama InputType
.
TypeScript
// src/tasks/dto/create-task.input.ts
import { InputType, Field } from '@nestjs/graphql';
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
@InputType()
export class CreateTaskInput {
@Field()
@IsNotEmpty()
@MinLength(3)
title: string;
@Field()
@IsString()
description: string;
}
Y ahora, añadimos la mutación al resolver:
TypeScript
// src/tasks/tasks.resolver.ts
// ... (imports)
import { Mutation, Args } from '@nestjs/graphql';
import { CreateTaskInput } from './dto/create-task.input';
@Resolver(() => TaskType)
export class TasksResolver {
// ... (constructor y query tasks)
@Mutation(() => TaskType)
@UseGuards(GqlAuthGuard)
createTask(
@Args('createTaskInput') createTaskInput: CreateTaskInput,
@GetUser() user: User,
) {
// Reutilizamos nuestro DTO de REST y el servicio transaccional
return this.tasksService.createTaskWithTags(createTaskInput, user);
}
}
El decorador @Args
nos permite recibir los argumentos de la mutación.
Paso 5: Probando en el Playground
Reinicia tu aplicación y vuelve al GraphQL Playground.
- Obtén un token de autenticación haciendo un
POST
a/auth/login
con Postman. - En el Playground, en la esquina inferior izquierda, abre la pestaña HTTP HEADERS y añade:JSON
{ "Authorization": "Bearer TU_TOKEN_AQUI" }
- Prueba la Query: Pega esto en el panel izquierdo y dale a “Play”.GraphQL
query { tasks { id title status } }
¡Observa cómo solo te devuelve los campos que pediste! - Prueba la Mutation:GraphQL
mutation { createTask(createTaskInput: { title: "Aprender GraphQL", description: "Completar el tutorial de NestJS" }) { id title } }
¡Felicidades! Has construido tus primeros endpoints de GraphQL reutilizando toda la lógica de negocio y seguridad que ya tenías.
5. Relaciones en el Grafo: Conectando Tareas y Usuarios
Hemos expuesto las tareas, pero ¿qué hay de los usuarios que las crearon? En una API REST, tendríamos que hacer una segunda petición para obtener la información del usuario. Con GraphQL, podemos navegar por el “grafo” de nuestros datos y pedir entidades relacionadas en una sola consulta.
Paso 1: Exponer la Entidad User
Primero, necesitamos que GraphQL sepa qué es un User
. Al igual que con TaskType
, crearemos un UserType
para definir su forma en el esquema.
Crea el archivo src/auth/dto/user.type.ts
:
TypeScript
// src/auth/dto/user.type.ts
import { ObjectType, Field, ID } from '@nestjs/graphql';
@ObjectType('User')
export class UserType {
@Field(() => ID)
id: string;
@Field()
username: string;
// NOTA: No exponemos la contraseña.
}
Crucial: Fíjate que deliberadamente no incluimos el campo password
. El esquema de GraphQL actúa como una fachada segura que nos permite exponer solo los datos que queremos que sean públicos.
Paso 2: Añadir el Campo Relacionado a TaskType
Ahora, le diremos a nuestro TaskType
que tiene una relación con UserType
.
Modifica src/tasks/dto/task.type.ts
:
TypeScript
// src/tasks/dto/task.type.ts
import { ObjectType, Field, ID } from '@nestjs/graphql';
import { UserType } from '../../auth/dto/user.type'; // 1. Importar UserType
import { TaskStatus } from '../entities/task.entity';
@ObjectType('Task')
export class TaskType {
// ... (id, title, description, status fields)
@Field(() => UserType) // 2. Añadir el campo de la relación
user: UserType;
}
Si reinicias la aplicación y revisas el esquema en el Playground, verás un error. NestJS sabe que hay un campo user
en TaskType
, pero no tiene idea de cómo obtener esos datos. Aquí es donde entra la magia de los resolvers de campo.
Paso 3: Creando un Field Resolver
Un Field Resolver es un método especializado que le enseña a GraphQL cómo resolver un campo específico y complejo dentro de un tipo. En este caso, le enseñaremos a resolver el campo user
de una Task
.
Añade este método dentro de tu TasksResolver
:
TypeScript
// src/tasks/tasks.resolver.ts
import { Resolver, Query, Parent, ResolveField } from '@nestjs/graphql';
// ... otros imports
import { UserType } from '../auth/dto/user.type';
@Resolver(() => TaskType) // Le dice a NestJS que este resolver es para TaskType
export class TasksResolver {
// ... (constructor y métodos existentes)
@ResolveField('user', () => UserType) // 1. Definir el resolver de campo
async getUser(@Parent() task: Task): Promise<User> {
// 2. Aquí TypeORM ya ha cargado la relación gracias a eager: true
// Si no fuera eager, tendríamos que cargarla manualmente.
// ej: return this.usersService.findById(task.userId);
return task.user;
}
}
Desglose de la Magia:
@ResolveField()
: Este decorador le indica a NestJS que el siguiente método es responsable de resolver un campo (user
) dentro del tipo padre (TaskType
).@Parent()
: Este decorador inyecta la instancia del objeto padre. Cuando GraphQL está procesando una tarea,@Parent()
nos da acceso a esa tarea específica.
Gracias a la relación eager
que configuramos en nuestra entidad Task
con User
en el tutorial anterior, el objeto task
que recibimos aquí ya viene con la propiedad user
cargada desde la base de datos. ¡Solo tenemos que devolverla!
Paso 4: Ejecutando una Query Anidada
Vuelve al GraphQL Playground. Ahora puedes ejecutar una consulta que antes era imposible. Pide una tarea y, dentro de ella, pide los detalles de su usuario.
GraphQL
query {
tasks {
id
title
user {
id
username
}
}
}
Observa el resultado. Con una sola petición al servidor, has obtenido datos de dos tablas diferentes, perfectamente anidados y solo con los campos que solicitaste. Has eliminado por completo el problema del “under-fetching”. Este es el verdadero superpoder de GraphQL en acción.
6. Preguntas y Respuestas (FAQ)
1. ¿GraphQL va a reemplazar a REST por completo?
No, no lo parece. Piensa en ellos como dos herramientas diferentes para trabajos distintos. REST es fantástico por su simplicidad, el uso del caché nativo de HTTP y su enorme ecosistema. Es ideal para APIs sencillas, orientadas a recursos y públicas. GraphQL brilla en aplicaciones complejas con datos interconectados, como aplicaciones móviles o frontends modernos (SPAs), donde la eficiencia de la red y la flexibilidad del cliente son críticas. Muchas empresas usan ambos.
2. ¿Puedo tener una API REST y una GraphQL en la misma aplicación NestJS?
¡Sí, y es una de las grandes ventajas de NestJS! Como has visto en esta serie, puedes tener tus controladores REST en /tasks
y tu endpoint de GraphQL en /graphql
conviviendo en la misma aplicación sin ningún problema. Esto te permite migrar progresivamente o servir a diferentes tipos de clientes según sus necesidades.
3. ¿Cómo se manejan los errores en GraphQL?
A diferencia de REST, que usa códigos de estado HTTP (404, 401, etc.), las peticiones de GraphQL casi siempre devuelven un código de estado 200 OK
, incluso si hay errores. Los errores se entregan dentro de un array
especial llamado errors
en la respuesta JSON, junto a la data (que puede ser null
). NestJS maneja esto automáticamente, convirtiendo sus excepciones estándar (NotFoundException
, UnauthorizedException
) en errores formateados para GraphQL.
4. ¿Qué es el “problema N+1” en GraphQL y cómo se soluciona?
Es un problema de rendimiento común. Imagina que pides 10 tareas y para cada tarea pides su usuario. Una implementación ingenua haría 1 (para las tareas) + 10 (una por cada usuario) = 11 consultas a la base de datos. Esto es el problema N+1. La solución es una técnica llamada batching (agrupamiento), que se implementa con una herramienta llamada DataLoader. Esta herramienta recolecta todos los IDs de usuario que se necesitan y los carga todos en una sola consulta a la base de datos (SELECT * FROM users WHERE id IN (...)
), solucionando el problema.
5. ¿Para qué sirve Subscriptions
en GraphQL?
Si las Queries
son para leer y las Mutations
para escribir, las Subscriptions
son para escuchar. Permiten a un cliente suscribirse a eventos en tiempo real en el servidor a través de WebSockets. Por ejemplo, un cliente podría suscribirse a newTaskCreated
, y el servidor le enviaría los datos de la nueva tarea en el instante en que otro usuario la cree, sin que el cliente tenga que estar preguntando constantemente.
7. Puntos Relevantes a Recordar
- GraphQL le da el Poder al Cliente: La idea central es que el frontend pide exactamente los campos que necesita, eliminando el exceso de datos (over-fetching) y la necesidad de múltiples peticiones (under-fetching).
- Code-First es la Magia de NestJS: Olvídate de mantener archivos de esquema por separado. Con la estrategia
code-first
, defines tu API de GraphQL usando los decoradores de TypeScript que ya conoces, y NestJS genera el esquema por ti. - Resolvers son los Nuevos Controladores: Toda la lógica para una entidad (
Task
,User
) se agrupa en un Resolver. Dentro de él, usas@Query
para operaciones de lectura y@Mutation
para operaciones de escritura. - Los Field Resolvers Conectan el Grafo: Para resolver campos complejos o relaciones (como el campo
user
dentro de unaTask
), se usa un@ResolveField
. Este método le enseña a GraphQL cómo obtener los datos para esa propiedad específica. - Reutiliza tu Lógica y Seguridad: No tienes que reescribir nada. Puedes llamar a los mismos servicios que usan tus controladores REST y proteger tus queries y mutations con los mismos
Guards
de NestJS que ya habías creado.
8. Conclusión
¡Felicidades! Has cruzado la frontera y has añadido un nuevo y poderoso paradigma a tu arsenal de desarrollo backend. Tu aplicación ya no solo responde a las peticiones estructuradas de REST, sino que ahora también ofrece un grafo de datos flexible y eficiente a través de GraphQL.
Has aprendido a pensar de una manera diferente: no en términos de endpoints, sino en un esquema de tipos y relaciones. Has visto de primera mano cómo darle el poder al cliente para que pida exactamente lo que necesita, optimizando la comunicación y haciendo tus aplicaciones más rápidas y eficientes. Lo mejor de todo es que has logrado esto reutilizando toda la lógica de negocio y seguridad que ya habías construido, demostrando la increíble modularidad de NestJS.
Ahora estás en una posición privilegiada, capaz de analizar los requisitos de un proyecto y tomar una decisión informada: ¿necesito la simplicidad y el caché de REST, o la flexibilidad y eficiencia de GraphQL? A menudo, como hemos visto, la respuesta es: ¿por qué no ambos?
9. Recursos Adicionales
Para continuar tu maestría en GraphQL, estos recursos son indispensables.
- Documentación de GraphQL en NestJS 📖: La fuente de verdad para entender todas las integraciones, opciones y patrones avanzados que NestJS ofrece.
- GraphQL.org 🌐: El sitio web oficial de GraphQL. Ideal para solidificar los conceptos fundamentales del lenguaje, independientemente del framework.
- Documentación de Apollo Server 🚀: Ya que NestJS lo usa por debajo, conocer las opciones avanzadas de Apollo te dará aún más poder para configurar tu servidor.
10. Sugerencias de Siguientes Pasos
Tu API de GraphQL es funcional, pero el ecosistema es vasto. Aquí tienes tres áreas para llevar tus habilidades al siguiente nivel:
- Implementar Paginación en GraphQL: Nuestra query
tasks
podría devolver miles de resultados. Investiga cómo implementar paginación en GraphQL (usando argumentos comolimit
yoffset
o el patrón de “conexiones” con cursores) para manejar grandes conjuntos de datos de forma eficiente. - Resolver el Problema N+1 con DataLoader: Como mencionamos en el FAQ, las consultas anidadas pueden llevar a un rendimiento deficiente. Aprende a usar la librería
DataLoader
para agrupar y optimizar las peticiones a tu base de datos, solucionando el problema N+1 de forma elegante. - Crear una
Subscription
para Eventos en Tiempo Real: Sumérgete en el mundo del tiempo real. Implementa unaSubscription
que notifique a los clientes conectados a través de WebSockets cada vez que se cree una nueva tarea. Es la puerta de entrada a aplicaciones interactivas y colaborativas.
11. Invitación a la Acción 💪
La teoría te ha dado el mapa, pero la verdadera exploración comienza ahora. No dejes que este conocimiento se quede en un tutorial.
¡Expande tu grafo!
Toma tu proyecto y empieza a conectar los puntos que faltan:
- Expón las Etiquetas: Crea un
TagType
y unTagsResolver
para permitir que los clientes consulten todas las etiquetas disponibles. - Conecta las Tareas con las Etiquetas: Modifica tu
TaskType
para incluir un campotags
y crea elField Resolver
correspondiente. - Crea una Mutación Compleja: Implementa una mutación
assignTagToTask
que conecte una etiqueta con una tarea.
Al construir estas conexiones por tu cuenta, no solo afianzarás lo aprendido, sino que empezarás a pensar de forma nativa en grafos de datos, una habilidad increíblemente valiosa en el desarrollo moderno.
¡Feliz construcción de grafos!