Diseñar la aplicación para que funcione con distintos servidores GraphQL
"Programar contra interfaces, no contra implementaciones" es la práctica de invocar una funcionalidad no directamente, sino a través de un contrato que enumera las entradas necesarias y el resultado esperado, ocultando cómo se realiza la implementación. Esta estrategia ayuda a desacoplar la aplicación de una implementación, proveedor o stack específicos, permitiendo intercambiarlos sin tener que modificar el código de la aplicación.
Podemos aplicar esta estrategia también con GraphQL. GraphQL puede actuar como intermediario entre la aplicación y el servidor, permitiéndonos ejecutar todas las modificaciones necesarias únicamente sobre las consultas GraphQL, manteniendo la lógica de negocio intacta.
Una consulta GraphQL actúa como interfaz entre el cliente y el servidor. Cuando se ejecuta una consulta, el servidor GraphQL la procesará y devolverá los datos requeridos al cliente. ¿De dónde provienen los datos? ¿Cómo se obtuvieron? El cliente no lo sabe, y no le importa.

La respuesta a la consulta tendrá la misma forma que la consulta. Para esta consulta GraphQL:
{
post(by: { id: 1 }) {
id
title
}
}...la respuesta será:
{
"data": {
"post": {
"id": 1,
"title": "Hello world!"
}
}
}Dada la misma consulta con diferentes parámetros, los datos devueltos serán distintos, pero la forma será constante. Esto significa que, mientras la consulta no cambie, la aplicación no necesita cambiar su lógica respecto a cómo leer y procesar los datos, y de igual modo no importará qué servidor GraphQL esté ejecutando la consulta.
Y así podemos intercambiar un servidor GraphQL por otro sin esfuerzo.
Las consultas dependen del esquema GraphQL
Ahora bien, el último párrafo es algo optimista, porque la consulta GraphQL puede tener que cambiar dependiendo del servidor GraphQL. Para ser más precisos, la consulta se basa en el esquema GraphQL, y si servidores distintos exponen esquemas distintos, entonces la consulta también será distinta.
Por ejemplo, un servidor GraphQL que use la Cursor Connections Specification puede ejecutar la siguiente consulta:
{
categories(first: 10000) {
edges {
node {
categoryId
description
id
name
slug
}
}
}
}Y otro servidor que use paginación al estilo WordPress (como Gato GraphQL) ejecutará la misma consulta así:
{
postCategories(pagination: { limit: 10000 }) {
id
description
globalID
name
slug
}
}Podemos apreciar las diferencias entre las dos consultas:
| Característica | Servidor #1 | Servidor #2 |
|---|---|---|
| Campo de categorías de post | categories | postCategories |
| Argumento de campo para limitar el número de resultados | first | pagination.limit |
El campo id de un objeto representa | su ID global único | su ID único para su tipo |
| Forma de la consulta | más profunda por edges.node | más plana |
Sustituir la consulta del primer servidor por la equivalente del segundo dentro de la aplicación, por sí solo, no funcionará. Eso es porque la lógica seguirá accediendo a los datos de la respuesta según la forma y los campos de la consulta original.
Una posible solución es reemplazar también la lógica para recuperar los datos en el cliente. Por ejemplo, la siguiente lógica:
const categories = data?.data.categories.edges.map(({ node = {} }) => node);...puede reemplazarse así:
const categories = data?.data.postCategories;Pero eso es precisamente lo que queremos evitar. Queremos mantener los cambios al mínimo, modificando únicamente la interfaz (la consulta GraphQL) y manteniendo la lógica de negocio sin alterar.
Afortunadamente, es posible superar las diferencias modificando solo las consultas GraphQL, siguiendo estos pasos:
- Mantener las consultas GraphQL separadas de la aplicación
- Adaptar los nombres de los campos mediante alias
- Adaptar la forma de la respuesta mediante un campo
self
Veamos cómo, mediante estos 3 pasos, podemos adaptar una aplicación para que apunte a un servidor GraphQL distinto.
Mantener las consultas GraphQL separadas de la aplicación
Separar las consultas GraphQL de la lógica de la aplicación implica:
- Almacenar cada consulta GraphQL (o un conjunto de ellas) en un archivo aparte, y todos ellos en una carpeta específica
- Exportar las consultas e importarlas en la aplicación
Por ejemplo, podemos colocar cada consulta GraphQL en un archivo aparte bajo src/data, y exportarla:
// file `src/data/categories.js`
export const QUERY_ALL_CATEGORIES = gql`
{
categories(first: 10000) {
edges {
node {
databaseId
description
id
name
slug
}
}
}
}
`;La aplicación puede entonces importar y usar la consulta GraphQL:
import { QUERY_ALL_CATEGORIES } from 'data/categories';
export async function getAllCategories() {
const apolloClient = getApolloClient();
const data = await apolloClient.query({
query: QUERY_ALL_CATEGORIES,
});
const categories = data?.data.categories.edges.map(({ node = {} }) => node);
return {
categories,
};
}Gracias a esta configuración, todas las modificaciones solo deben realizarse sobre los archivos bajo src/data.
Adaptar los nombres de los campos mediante alias
Se puede usar un alias de campo para renombrar un campo de la respuesta del segundo servidor GraphQL al nombre de ese campo en el primer servidor.
De esta forma, los campos postCategories, id y globalID se pueden recuperar usando los nombres que espera la aplicación: categories, categoryId e id respectivamente:
{
categories: postCategories(pagination: { limit: 10000 }) {
categoryId: id
description
id: globalID
name
slug
}
}Hay que tener en cuenta que el campo categories tiene argumento first, mientras que su campo correspondiente postCategories usa el argumento pagination.limit. Sin embargo, dado que los argumentos del campo no se reflejan en el nombre del campo en la respuesta, no nos tenemos que preocupar por ellos.
Adaptar la forma de la respuesta mediante un campo self
El último reto es algo más complicado: necesitamos modificar la forma de la respuesta, añadiendo los niveles adicionales para edges y node provenientes de la spec Cursor Connections.
Para lograrlo, introduciremos un campo self en todos los tipos del esquema GraphQL, que devuelve el mismo objeto sobre el que se aplica:
type QueryRoot {
self: QueryRoot!
}
type Post {
self: Post!
}
type User {
self: User!
}El campo self permite añadir niveles adicionales a la consulta sin salir del objeto consultado. Al ejecutar esta consulta:
{
__typename
self {
__typename
}
post(by: { id: 1 }) {
self {
id
__typename
}
}
user(by: { id: 1 }) {
self {
id
__typename
}
}
}...se produce esta respuesta:
{
"data": {
"__typename": "QueryRoot",
"self": {
"__typename": "QueryRoot"
},
"post": {
"self": {
"id": 1,
"__typename": "Post"
}
},
"user": {
"self": {
"id": 1,
"__typename": "User"
}
}
}
}Ahora, podemos usar self para añadir artificialmente los niveles nodes y edge:
{
categories: self {
edges: postCategories(pagination: { limit: 10000 }) {
node: self {
categoryId: id
description
id: globalID
name
slug
}
}
}
}El tipo del objeto en el esquema GraphQL para edges y para self es obviamente distinto. Pero eso no le importa a la aplicación, porque no interactúa con el objeto real modelado en el servidor GraphQL. En su lugar, recibe los datos como un objeto JSON, y ese fragmento de datos para un campo proveniente de un objeto PostConnection o de un objeto Post será el mismo.
Hay que tener en cuenta que el campo categories se resuelve mediante self y edges se resuelve mediante postCategories, y no al revés. Esto es para mantener que la cardinalidad de los elementos devueltos coincida con la definida por los campos que usan la spec Cursor Connections:
type RootQuery {
categories: RootQueryToCategoryConnection
}
type RootQueryToCategoryConnection {
edges: [RootQueryToCategoryConnectionEdge]
}
type RootQueryToCategoryConnectionEdge {
node: Category
}Si la consulta GraphQL adaptada fuera al revés (es decir, consultando categories: postCategories y edges: self), el acceso a los datos fallaría, porque data.categories sería un array, así que data.categories.edges lanzaría un error al ejecutar:
const categories = data?.data.categories.edges.map(({ node = {} }) => node);Adaptar todas las consultas
Tras aplicar la misma estrategia a todas las consultas GraphQL en src/data, la aplicación puede intercambiar fácilmente entre un servidor GraphQL y otro.