Probablemente, hayas oído hablar de GraphQL. Entre la maraña de tecnologías que nos abruman, quizás no has tenido tiempo para investigar de qué se trataba. ¿Y conoces Spring, el framework que ha conquistado el desarrollo de software con Java por su ligereza, conveniencia y diversidad de módulos?

En este post os contaremos el modo en que estos dos mundos se han entrelazado a partir del lanzamiento de Spring for GraphQL (en mayo de 2022). Recién lanzada su versión 1.2.0, se trata de un conjunto de funcionalidades orientadas a facilitar el desarrollo y testeo de APIs de alto rendimiento.

¿Qué es GraphQL?

Spring for GraphQL es un proyecto desarrollado en conjunto por la comunidad Spring y el equipo de GraphQL Java para trabajar con GraphQL en este lenguaje de programación. Existen implementaciones de GraphQL para muchos otros lenguajes, ya que una de sus características es no estar acoplado a este nivel.

Pero ¿qué es GraphQL? Sería útil pensarlo como un lenguaje cotidiano, como el inglés o el francés. Con GraphQL un cliente se puede comunicar con un servidor para pedirle exactamente los datos que necesita, pues conoce de antemano qué le puede ofrecer este. El contrato que estipula lo que una API GraphQL ofrece se conoce como schema. Veamos un ejemplo y examinemos sus componentes:

type Query {
    artistas: [Artista]
    artistasPorEstilo(estilo: String): [Artista]
}

type Mutation {
    agregarArtista(nuevo: ArtistaInput): Artista
}

type Artista {
    id: ID
    apellido: String
    estilo: String
}

input ArtistaInput {
    apellido: String
    estilo: String
}

Desglosaremos algunas palabras clave que podemos ver aquí. En primer lugar, las Query son búsquedas, operaciones de solo lectura; las Mutations, son operaciones de alta, baja o modificación. Entre estas dos palabras se dividen las tradicionales operaciones CRUD. Además, existe un tercer tipo soportado por GraphQL -Subscription- que sirve para la recepción continua de datos de una fuente emisora (típicamente asíncrona).

En estos bloques tenemos, del lado izquierdo de los dos puntos, el nombre de nuestra operación y sus parámetros, si los tiene; del lado derecho, la devolución. Toda operación en GraphQL debe retornar algo, ya sea una unidad (Artista) o varias ([Artista]). Las Query pueden o no recibir argumentos (artistasPorEstilo y artistas, respectivamente).

Las Mutation siempre los necesitan: nuestro ejemplo recibe un ArtistaInput, un tipo customizado de nuestra aplicación que está definido más abajo, y nos devuelve otro tipo, Artista, similar salvo por el ID que será autogenerado (en esta pregunta de Stack Overflow tenéis una excelente explicación de la diferencia (que a muchos inicialmente nos pareció innecesaria) entre type e input.).

Los demás type y los input son las definiciones propias de nuestro proyecto, que contienen campos que pueden ser de los tipos scalar (String, Int, Enum) o de otros tipos customizados.

GraphQL es agnóstico de lenguajes de programación, en el mismo sentido que lo es una definición OpenAPI. Con este contrato podríamos generar un servidor en Java, así como construir un cliente en Javascript o en muchos otros lenguajes.

Spring al rescate

Antes de Spring for GraphQL ya existían librerías para crear servidores y clientes GraphQL en Java (la más conocida quizás sea Apollo), pero no eran fáciles de usar porque la generación de los resolver (es decir, los métodos que conectaban el contrato de la API con los objetos Java) era a muy bajo nivel, engorrosa y propensa a errores.

Con este módulo (que podemos incorporar fácilmente como starter desde start.spring.io) podremos crear fácilmente métodos para atender queries, mutations y subscriptions con una semántica muy parecida a Spring MVC (es compatible tanto con este tipo de arquitectura como con la non-blocking de Spring Webflux).

Continuando con el ejemplo anterior, creamos un primer Controller:

@Controller
class GraphqlController{

   @QueryMapping
   Flux<Artista> artistas(){      
      return Flux.fromIterable(bbdd)
            .delaySubscription(Duration.of(1, ChronoUnit.SECONDS))
            .doOnSubscribe(e->log.info("Suscrito query general: "+ Instant.now().get(ChronoField.MILLI_OF_SECOND)))
            .doOnComplete(()-> log.info("Completado query general."));
   }
}

Anotada con el clásico @Controller de Spring, esta clase contendrá los resolver para las operaciones definidas en nuestro schema. El nombre del método es por defecto el mismo que el de la query, en este caso (se puede customizar); el tipo de devolución puede ser tanto blocking (List) como non-blocking (Flux). Si no conocéis el mundo reactivo no os preocupéis, ya que para el mismo schema este método podría elegir devolver una List sin problemas.

En el caso de una query con argumentos, se pasa como argumento del método anotado con @Argument (valga la redundancia). Nótese asimismo como tanto el nombre del método como el del argumento son en este caso distintos de lo declarado en el schema, pero se le pasa dicho literal a ambas anotaciones (@Argument y @QueryMapping):

@QueryMapping("artistasPorEstilo")
List<Artista> artistsByStyle(@Argument(name = "estilo") String style){ 
   return bbdd.stream()
        .filter(artista -> artista.estilo().equalsIgnoreCase(style))
        .toList();
}

Por último, para declarar una mutation (lo que en REST podrían ser POST, PUT, PATCH ó DELETE) la sintaxis es similar:

@MutationMapping
Mono<Artista> agregarArtista (@Argument ArtistaInput nuevo) {
   var nuevoArtista = crearArtistaDesdeInput(nuevo);
   return Mono.just(nuevoArtista);
}

El tipo de argumento no es Artista sino ArtistaInput. Dentro de Java podríamos solucionar la diferencia entre nuestro input y nuestro type de muchas otras maneras, pero esta es en mi opinión la más legible y mantenible: crear un nuevo record.

record ArtistaInput (String apellido, String estilo){}

Testeando el servidor

Para testear nuestra servidor GraphQL con Spring tendremos muchas facilidades. Lo primero que tenemos que hacer es crearnos una clase de test indicando el nombre del controller que queremos probar:

@GraphQlTest(GraphqlController.class)
class GraphqlServerApplicationTests {
}

Luego inyectamos nuestro tester, que está registrado ya como Bean en el contexto de Spring:

@Autowired
private GraphQlTester graphQlTester;

A continuación debemos crear nuestros ficheros .graphql que servirán de mock para lo que queramos probar, por ejemplo las queries. Deben colgar de test/resources/graphql-test:

Ficheros .graphql que servirán de mock.

El contenido de estos ficheros será el mismo que el de una hipotética consulta al servidor, por ejemplo:

query {
    artistas{
        apellido
    }
}

En la clase de test haremos referencia a este documento mediante el método documentName(); también podríamos insertar el literal de la consulta, pero no es tan buena práctica.

@Test
void debeHaberTresArtistas(){
   this.graphQlTester
         .documentName("artistas_test1")
         .execute()
         .path("artistas")
         .entityList(Artista.class)
         .hasSize(3);
}

Así luego de ejecutar la instrucción del documento, le indicamos que el fragmento que queremos evaluar es el que pende de “artistas”; si quisiéramos evaluar un nivel más interior, haríamos referencia con corchetes:

.path("artistas[0].obras")

Allí dentro serializaremos el contenido considerando que será del tipo Artista, una List específicamente. Lo que esperamos, por último, es que contenga tres artistas.

Clientes

Por último, os mencionamos que la creación de un cliente con Spring for GraphQL es muy similar a la de clientes web reactivos (WebClient).

Definimos un Bean:

@Bean
HttpGraphQlClient graphQlClient(){
   return HttpGraphQlClient.builder()
         .url("http://localhost:8181/graphql")
         .build();
}

Luego en donde lo necesitemos, lo inyectamos y aprovechamos sus métodos para realizar las queries, mutations o subscriptions.

A continuación

Impresionante, ¿verdad? Gran parte de la configuración automática es editable y parametrizable, pero en esta demo apenas estamos escarbando la superficie.

En este webinar presenté el desarrollo de una aplicación con Spring for GraphQL desde cero, en vivo, y podréis comprender mejor el proceso.

En estos repositorios de Github, Spring for GraphQL - Server y Spring for GraphQL – client, os compartimos dos aplicaciones completas, un servidor y un cliente, para que podáis replicar lo aprendido y desarrollarlo un poco más. Veréis algunos cambios, como por ejemplo la seguridad (Spring Security) o la referencia a entidades anidadas con @SchemaMapping y @BatchMapping (para evitar el problema de N+1).

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete