Deprecación en GraphQL sin número de versión
6 min de lectura
Una API REST puede publicar /v2/ junto a /v1/ y dejar que cada consumidor migre a su propio
ritmo. GraphQL tiene un esquema en un endpoint, y cada cliente, la app móvil con el build del año
pasado y el dashboard interno desplegado esta mañana, consulta el mismo grafo. No hay URL que
bifurcar. Deprecar un campo significa marcarlo como deprecado en el mismo sitio, en un esquema del
que todo el mundo ya depende, lo que hace que la disciplina sea distinta de REST aunque el
problema de fondo, decirle a quien consume que algo va a desaparecer, sea el mismo que cubre
deprecación de API en general.
¿Cómo marca GraphQL un campo como deprecado, si no hay versión que subir?
Con la directiva @deprecated, aplicada directamente sobre el campo:
type Product {
price: Float @deprecated(reason: "Use priceV2 for multi-currency support.")
priceV2: Money
}
El campo sigue siendo consultable. No desaparece, no devuelve un 404, no cambia de comportamiento; solo lleva una nota legible por máquina que la mayoría de herramientas GraphQL, GraphiQL, Apollo Studio, linters de esquema, mostrarán a quien navegue el esquema o escriba una query contra él. Ese es todo el mecanismo. No hay un endpoint de deprecación separado, ni cabecera, ni documento adicional exigido por la especificación, lo cual es a la vez el atractivo y la trampa: la directiva es fácil de añadir y fácil de ignorar, porque nada obliga a un cliente a fijarse en ella.
¿Alguien llega a ver realmente el motivo de la deprecación?
Solo quien usa el esquema directamente, por introspección o un editor consciente del esquema, y
ese es un público más pequeño que el lector habitual de un changelog de API. Una app móvil
construida contra una query hace seis meses ya tiene esa query horneada en su binario; seguirá
pidiendo price y seguirá recibiendo una respuesta, deprecado o no, hasta que alguien reconstruya
la app con el campo nuevo y publique una actualización. La directiva le dice a una desarrolladora
que escribe código nuevo que no use el campo viejo. No hace nada por el cliente que ya está
publicado y en funcionamiento.
| Mecanismo | A quién alcanza |
|---|---|
Directiva @deprecated | Desarrolladoras navegando el esquema o escribiendo queries nuevas |
| Fallos de CI del linter de esquema | El equipo dueño del código cliente, si tiene uno configurado |
| Una entrada de changelog | Quien la lea, incluido un equipo cliente sin linter |
| Nada (el campo simplemente funciona) | Un cliente ya construido que usa el campo antiguo |
¿Debería un campo deprecado tener también una entrada de changelog?
Sí, y hace más trabajo que la directiva sola, porque un changelog alcanza a gente que la directiva no puede: un equipo socio que consume el grafo sin navegar su esquema, un cliente construido contra una copia cacheada del esquema de hace meses, cualquiera que solo se enteraría leyendo prosa. Changelog de API cubre en general qué le debe una entrada a quien consume; una entrada de GraphQL debe una cosa que REST rara vez tiene que explicitar, porque quien consume REST la infiere del número de versión: si el campo antiguo todavía funciona hoy, todavía funciona con una advertencia, o de hecho ha dejado de devolver datos. La directiva sola no responde nada de eso para quien nunca abrió el esquema.
¿Cuándo es realmente seguro eliminar un campo del esquema?
Solo cuando los logs de queries muestran que nadie lo pide, que es una pregunta de uso, no de
calendario. Un campo puede llevar @deprecated durante un año y seguir siendo crítico para un
cliente que nunca se reconstruyó; eliminarlo en un calendario fijo, como suele hacer un
Sunset de REST, rompe ese cliente sin ninguna advertencia sobre la que pueda actuar, porque
GraphQL no le da nada sobre lo que actuar más allá de la directiva que nunca leyó. Registrad el
uso a nivel de campo antes de comprometeros con una fecha de eliminación, y tratad cualquier
recuento de queries distinto de cero como una pausa, no como una cuenta atrás.
¿Añadir un campo tiene el mismo riesgo que en una API REST?
Menos, para un campo nuevo, porque un cliente GraphQL solo recibe los campos que pide
explícitamente. Añadir priceV2 junto a price no puede romper una query existente de la forma
en que añadir un campo a una respuesta JSON de REST puede romper un deserializador estricto,
porque nada obliga al cliente a pedir el campo nuevo. Añadir un valor a un enum existente es la
excepción que vale la pena nombrar en el mismo aliento: un cliente que distingue exhaustivamente
cada valor del enum, algo que los lenguajes fuertemente tipados fomentan, se rompe en cuanto llega
un valor nuevo, lo haya pedido o no alguna query. La seguridad solo se sostiene para campos y
miembros de union en los que el cliente opta explícitamente; no se sostiene para un conjunto
cerrado que el código del cliente enumera a mano.
¿Qué necesita una entrada de changelog de GraphQL que una de REST no?
La forma de la query, no solo el nombre del campo, porque “el campo price está deprecado” le
falta la pieza que quien consume realmente necesita: qué tipos y qué queries lo tocan. Una
entrada útil nombra el tipo, el campo, el campo de reemplazo y, si podéis generarlo, las queries
reales en producción que todavía piden la forma antigua. Esa última pieza, atar el aviso de
deprecación al uso real, es lo que quien consume REST obtiene gratis de los logs del servidor
sobre una URL y quien consume GraphQL no, porque cada query golpea el mismo endpoint sin importar
qué pida.
¿Algo más allá de un campo puede llevar la directiva @deprecated?
Los valores de enum, usando la misma directiva en la propia definición del valor en vez de en la del campo:
enum ShippingMethod {
STANDARD
EXPRESS
OVERNIGHT @deprecated(reason: "Use EXPRESS with priority: true instead.")
}
La especificación define @deprecated para exactamente dos ubicaciones, una definición de campo o
un valor de enum, y nada más en la versión estable; la deprecación a nivel de argumento o de campo
de input solo existe en lenguaje de borrador posterior, no en lo que implementan hoy la mayoría de
los servidores. Un valor de enum marcado así sigue siendo un valor legal que un servidor puede
seguir devolviendo o aceptando, la misma promesa de no ruptura que hace un campo deprecado, que es
lo que permite lanzarlo con seguridad antes de eliminar el valor de verdad.
FAQ
¿GraphQL soporta algo parecido a un header Sunset para todo un endpoint?
No, porque normalmente solo hay un endpoint. El calendario de deprecación vive a nivel de campo,
en el texto del motivo de la directiva @deprecated y en el changelog o guía de migración que un
equipo publique junto a él, no en una cabecera de respuesta que un cliente pueda leer
programáticamente.
¿Se puede eliminar un campo deprecado y volver a añadirlo después con un tipo distinto?
Solo con un nombre de campo nuevo. Reintroducir el mismo nombre de campo con un tipo cambiado es
exactamente el breaking change que el ciclo de deprecación existe para evitar; dadle al
reemplazo su propio nombre, como hace priceV2, y dejad que el antiguo se extinga por completo
antes de que el nombre quede libre para reutilizarse.
¿Debería el texto del motivo de @deprecated enlazar a la entrada de changelog?
Sí, cuando las herramientas del esquema lo permitan. El campo de motivo acepta una cadena de
texto simple, y una URL dentro de esa cadena es el camino más corto desde una desarrolladora
mirando la salida de introspección hasta la explicación más completa que puede dar una entrada de
changelog.
¿Un cambio de esquema en GraphQL es alguna vez compatible hacia atrás de una forma que REST no lo es? Los cambios aditivos de campo, sí, por la razón de arriba: quien consume solo recibe lo que pide. Los valores nuevos de enum son la excepción, porque un cliente que enumera un conjunto cerrado puede romperse con uno que no esperaba. Las eliminaciones y los cambios de tipo son exactamente tan rompedores como sus equivalentes REST.
Las afirmaciones técnicas de este artículo no se han revisado de forma independiente. Si algo está mal, avísanos y lo corregiremos.