Lección 4 de 511 min
GraphQL SDL: definir el esquema
Tipos, nulabilidad, listas, consultas, mutaciones y suscripciones: el contrato que comparten cliente y servidor.
Imagina una app de recetas con una web y una app móvil. La web quiere cada receta con su autor y sus comentarios; la app móvil, solo el título y la foto. Con GraphQL, cada cliente pide exactamente los campos que necesita en una sola petición. Pero para que eso funcione, cliente y servidor tienen que estar de acuerdo en qué datos existen y qué forma tienen.
Ese acuerdo es el esquema, y se escribe en SDL (Schema Definition Language), un lenguaje pequeño y muy legible que solo describe tipos: no contiene lógica. En esta lección aprenderás a leerlo y a escribirlo. En el módulo de APIs verás cómo se implementa el servidor y cómo se compara GraphQL con REST.
Tipos, campos y nulabilidad
Tipos objeto y escalares
Los bloques del esquema: tipos con campos y, en las hojas, valores simples.
- Creado por:
- Facebook (hoy Meta)
- Publicado:
- 2015
- Extensión:
- .graphql
Un tipo objeto describe una cosa de tu dominio con sus campos: una Receta tiene titulo, minutos y un autor. Cada campo tiene un tipo, que puede ser otro tipo objeto. Así se forma un grafo por el que el cliente navega: de la receta a su autor, y del autor a sus otras recetas.
En los extremos del grafo están los escalares, los valores simples. GraphQL trae cinco: Int (entero de 32 bits), Float, String, Boolean e ID, un identificador único que viaja como texto. Puedes declarar los tuyos, como scalar Fecha, pero el servidor tendrá que definir cómo se validan y se convierten.
Úsalo cuando
Para cada entidad que el cliente pueda pedir. Ponle nombres pensados para quien consume la API, no copiados de tus tablas.
Ten en cuenta
Int no admite números mayores de algo más de 2000 millones: para identificadores largos usa ID y para cantidades grandes, Float o un escalar propio. Y recuerda que el SDL solo describe: el código que rellena cada campo (los resolvers) vive en el servidor.
# El ! significa «nunca es null»: lo verás en la siguiente ficha
type Receta {
id: ID!
titulo: String!
minutos: Int!
valoracion: Float
vegana: Boolean!
autor: Usuario!
}
type Usuario {
id: ID!
nombre: String!
recetas: [Receta!]!
}Nulabilidad y listas: ! y [ ]
Qué campos pueden faltar y cuáles devuelven varios elementos.
En GraphQL todos los campos admiten null por defecto. Un ! detrás del tipo lo convierte en no nulo: String! garantiza que siempre habrá un texto. Los corchetes indican una lista: [Receta] es una lista de recetas.
Al combinarlos, la posición del ! importa. [Receta!]! es una lista que siempre existe y cuyos elementos nunca son null. [Receta]! siempre devuelve una lista, pero puede tener huecos. [Receta!] puede ser null entera aunque, si llega, no tiene huecos.
Si un campo no nulo falla en el servidor, el null «sube» hasta el campo anulable más cercano y el error aparece en la lista errors de la respuesta. Por eso, marcar todo con ! hace que un fallo pequeño se lleve por delante una parte grande de la respuesta.
Úsalo cuando
Marca con ! lo que tu servidor pueda garantizar siempre, como el id o un título obligatorio. Para listas, [Tipo!]! es la opción más habitual.
Ten en cuenta
Quitar el ! de un campo de salida rompe a los clientes que contaban con él; añadirlo a un argumento rompe a los que no lo enviaban. Piénsalo bien desde el principio.
type Receta {
titulo: String! # siempre hay título
foto: String # puede no tener foto: null
etiquetas: [String!]! # siempre una lista, quizá vacía, sin nulls
comentarios: [Comentario] # la lista y sus elementos pueden ser null
}Nota
El esquema es también la documentación de la API. Herramientas como GraphiQL lo leen mediante introspección (el servidor responde preguntas sobre su propio esquema) y te ofrecen autocompletado y la lista de tipos mientras escribes la consulta.
Los puntos de entrada
Los tipos objeto describen los datos, pero el cliente necesita una puerta por la que entrar al grafo. Esas puertas son tres tipos especiales, llamados tipos raíz, y los campos que reciben datos del cliente.
Query, Mutation y Subscription
Los tipos raíz: leer datos, cambiarlos y recibir avisos en tiempo real.
- Obligatorio:
- Solo Query
Cada campo de Query es una forma de leer datos, como receta(id: ID!) o recetas. Cada campo de Mutation es una acción que cambia algo, como crearReceta, y suele devolver el objeto modificado para que el cliente actualice su pantalla sin otra petición. Si una petición incluye varias mutaciones, se ejecutan en orden, una tras otra, mientras que los campos de una consulta pueden resolverse en paralelo.
Subscription define eventos a los que el cliente se suscribe para recibir datos en tiempo real, como un comentario nuevo. El esquema solo dice qué eventos existen; el transporte, que suele ser WebSockets, lo decide el servidor. De los tres tipos raíz, solo Query es obligatorio.
Úsalo cuando
Query para todo lo que solo lee, Mutation para cualquier cambio y Subscription solo cuando el cliente necesite enterarse al instante, como en un chat o un marcador en directo.
Ten en cuenta
Nada impide técnicamente que un campo de Query modifique datos, pero rompe lo que esperan los clientes y las cachés. Y cada suscripción mantiene una conexión abierta: si basta con refrescar cada minuto, no la necesitas.
type Query {
receta(id: ID!): Receta
recetas: [Receta!]!
}
type Mutation {
crearReceta(datos: NuevaReceta!): Receta!
borrarReceta(id: ID!): Boolean!
}
type Subscription {
comentarioNuevo(recetaId: ID!): Comentario!
}Argumentos e input types
Cómo recibe datos un campo: argumentos con tipo y objetos de entrada.
Cualquier campo puede tener argumentos, cada uno con su tipo y, si quieres, un valor por defecto: recetas(limite: Int = 10, vegana: Boolean). El servidor valida los argumentos contra el esquema antes de ejecutar nada, así que un limite: "diez" se rechaza sin llegar a tu código.
Cuando los datos de entrada son muchos, se agrupan en un input type, que se declara con input en lugar de type. Parece un tipo objeto, pero solo sirve para entrada: sus campos pueden ser escalares, enums u otros inputs, nunca tipos objeto. Es lo habitual en las mutaciones.
Úsalo cuando
Argumentos sueltos para filtros y paginación sencillos; un input type para crear o editar entidades, donde llegan varios campos a la vez.
Ten en cuenta
No puedes usar un tipo objeto como entrada, y es mejor así: lo que el cliente envía (sin id ni fecha de creación) rara vez coincide con lo que recibe. Lo normal es tener Receta para la salida y NuevaReceta para la entrada.
input NuevaReceta {
titulo: String!
minutos: Int!
etiquetas: [String!] = []
}
type Mutation {
crearReceta(datos: NuevaReceta!): Receta!
}
# El cliente la usa así:
# mutation {
# crearReceta(datos: { titulo: "Gazpacho", minutos: 15 }) {
# id
# }
# }Atención
En el cliente, no pegues los valores dentro del texto de la consulta: declara variables ($datos: NuevaReceta!) y envíalas aparte, en el JSON de la petición. Es la misma idea que las consultas parametrizadas de SQL.
Enums, interfaces y uniones
A veces un campo no es un simple texto ni un único tipo: puede tomar uno de varios valores fijos, o devolver objetos de tipos distintos. Para eso hay tres herramientas, que conviene no confundir.
Enums
Una lista cerrada de valores permitidos para un campo o un argumento.
Un enum enumera los únicos valores válidos, como FACIL, MEDIA y DIFICIL. A diferencia de un String, el servidor rechaza cualquier otro valor, y las herramientas pueden autocompletar las opciones y generar tipos para tu código.
Por convención, los valores se escriben en mayúsculas. Sirven tanto en campos de salida como en argumentos, por ejemplo, para filtrar recetas por dificultad.
Úsalo cuando
Para estados, categorías o modos que conoces de antemano: el estado de un pedido, la dificultad de una receta, el criterio de ordenación.
Ten en cuenta
Añadir un valor nuevo puede romper a los clientes que tratan todos los casos con un switch y no esperan uno más. Si la lista cambia a menudo o la editan los usuarios, quizá encaje mejor un tipo objeto.
enum Dificultad {
FACIL
MEDIA
DIFICIL
}
type Receta {
titulo: String!
dificultad: Dificultad!
}
type Query {
recetas(dificultad: Dificultad): [Receta!]!
}Interfaces
Un conjunto de campos que varios tipos se comprometen a tener.
Una interface declara campos comunes, y cada tipo que la implementa con implements debe incluirlos todos. Por ejemplo, Receta y Articulo pueden implementar Publicacion, que exige id y titulo.
Un campo que devuelve una interfaz puede devolver cualquiera de esos tipos. El cliente pide los campos comunes directamente y los propios de cada tipo con un fragmento en línea: ... on Receta { minutos }.
Úsalo cuando
Cuando varios tipos comparten de verdad una parte de su forma y quieres listarlos juntos, como en un muro de publicaciones. Muchas APIs usan una interfaz Node con un id para poder pedir cualquier objeto por su identificador.
Ten en cuenta
No crees interfaces «por si acaso»: cada tipo que la implementa queda atado a sus campos. Si los tipos no tienen nada en común, lo que buscas es una unión.
interface Publicacion {
id: ID!
titulo: String!
}
type Receta implements Publicacion {
id: ID!
titulo: String!
minutos: Int!
}
type Articulo implements Publicacion {
id: ID!
titulo: String!
texto: String!
}
type Query {
muro: [Publicacion!]!
}Uniones
Un campo que puede devolver uno de varios tipos sin campos en común.
Una union dice «este campo devuelve una Receta, un Usuario o un Ingrediente», sin exigir que compartan ningún campo. Solo puede agrupar tipos objeto. Es el caso típico de un buscador que mezcla resultados de distinta naturaleza.
Como no hay campos garantizados, el cliente usa un fragmento por cada tipo que le interese y puede pedir el campo especial __typename para saber cuál le ha llegado.
Úsalo cuando
Para resultados heterogéneos, como búsquedas o notificaciones de varias clases, y para devolver errores como datos: union ResultadoPago = Pago | SaldoInsuficiente.
Ten en cuenta
Si te descubres repitiendo los mismos campos en cada fragmento, esos tipos comparten forma y una interfaz encaja mejor.
union ResultadoBusqueda = Receta | Usuario | Ingrediente
type Query {
buscar(texto: String!): [ResultadoBusqueda!]!
}
# El cliente pide campos según el tipo:
# {
# buscar(texto: "tomate") {
# __typename
# ... on Receta { titulo }
# ... on Ingrediente { nombre }
# }
# }Documentar y evolucionar el esquema
Descripciones y directivas
Documentación que forma parte del esquema y anotaciones como @deprecated.
- Las más vistas:
- @deprecated, @include, @skip
Una descripción es un texto entre comillas, o entre triples comillas si ocupa varias líneas, justo antes de un tipo, un campo o un argumento. A diferencia de un comentario con #, forma parte del esquema: la devuelve la introspección y la muestran herramientas como GraphiQL. Admite Markdown.
Las directivas empiezan por @ y añaden información o comportamiento. La más útil al definir un esquema es @deprecated(reason: "…"), que marca un campo como obsoleto sin borrarlo, para que los clientes migren con calma. En las consultas se usan @include y @skip para pedir campos según una condición, y puedes declarar directivas propias con directive.
Úsalo cuando
Describe todo lo que no sea evidente por su nombre, y usa @deprecated cada vez que quieras retirar un campo: GraphQL apuesta por evolucionar el esquema poco a poco en lugar de publicar una «v2».
Ten en cuenta
@deprecated no elimina nada: el campo sigue funcionando hasta que lo borres. Antes de hacerlo, comprueba en tus métricas o registros que ya nadie lo pide; lo verás en el módulo de observabilidad.
"""
Una receta publicada por un usuario.
Los borradores no aparecen aquí.
"""
type Receta {
id: ID!
titulo: String!
"Tiempo total en minutos, incluida la preparación."
minutos: Int!
duracion: Int! @deprecated(reason: "Usa minutos.")
}¿Sabías que…?
Hay dos formas de trabajar con el esquema. En schema-first escribes el SDL a mano y después los resolvers; en code-first defines los tipos en tu lenguaje (por ejemplo, con clases de TypeScript) y una librería genera el SDL. En ambos casos, el resultado es el mismo contrato.
Interactivo
Tres formas de decir «uno de varios»
Enums, interfaces y uniones resuelven problemas parecidos, pero no iguales.
| Característica | Enum | Interfaz | Unión |
|---|---|---|---|
| Qué representa | Un valor de una lista cerrada | Varios tipos que comparten campos | Varios tipos sin campos en común |
| Campos comunes | No tiene campos: es un valor | Obligatorios en todos los tipos que la implementan | Ninguno garantizado |
| Cómo se consulta | Directamente: devuelve el nombre del valor | Los comunes, directamente; los propios, con ... on Tipo | Solo con ... on Tipo, y __typename para saber cuál llegó |
| Puede ser argumento | Sí | No | No |
| Ejemplo | Dificultad: FACIL, MEDIA, DIFICIL | Publicacion, implementada por Receta y Articulo | ResultadoBusqueda = Receta | Usuario |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
El formulario «Nueva receta» envía título, minutos, ingredientes y etiquetas. Quieres que la mutación reciba todo eso como un único argumento con tipo.
Para recordar
- El esquema SDL es el contrato entre cliente y servidor: describe tipos y campos, no lógica.
- En GraphQL todo admite
nullsalvo que lo marques con!, y en las listas la posición del!cambia el significado. Querylee,Mutationcambia ySubscriptionavisa en tiempo real; soloQueryes obligatorio.- Los datos de entrada se declaran con
input; los enums cierran los valores posibles; las interfaces exigen campos comunes y las uniones no. - El esquema evoluciona sin versiones: añade campos nuevos y marca los viejos con
@deprecatedantes de quitarlos.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.¿Qué significa el tipo
[Comentario!]!?2.¿Por qué no puedes usar el tipo objeto
Recetacomo argumento decrearReceta?3.Quieres retirar el campo
duracionsin romper a los clientes que lo usan. ¿Qué haces primero?4.Un campo de tipo
String!falla al resolverse en el servidor. ¿Qué recibe el cliente?5.¿Qué diferencia hay entre una interfaz y una unión?