GraphQL frente a REST: un tutorial de GraphQL

Publicado: 2022-03-11

Es posible que haya oído hablar del nuevo chico de la cuadra: GraphQL. Si no, GraphQL es, en una palabra, una nueva forma de obtener API, una alternativa a REST. Comenzó como un proyecto interno en Facebook y, dado que era de código abierto, ganó mucha tracción.

El objetivo de este artículo es ayudarlo a hacer una transición fácil de REST a GraphQL, ya sea que ya se haya decidido por GraphQL o simplemente esté dispuesto a probarlo. No se necesita conocimiento previo de GraphQL, pero se requiere cierta familiaridad con las API REST para comprender el artículo.

GraphQL frente a REST: un tutorial de GraphQL

La primera parte del artículo comenzará dando tres razones por las que personalmente creo que GraphQL es superior a REST. La segunda parte es un tutorial sobre cómo agregar un punto final de GraphQL en su back-end.

Graphql frente a REST: ¿Por qué eliminar REST?

Si aún tiene dudas sobre si GraphQL se adapta o no a sus necesidades, aquí se brinda una descripción bastante extensa y objetiva de "REST vs. GraphQL". Sin embargo, para conocer mis tres razones principales para usar GraphQL, siga leyendo.

Razón 1: rendimiento de la red

Digamos que tiene un recurso de usuario en el back-end con nombre, apellido, correo electrónico y otros 10 campos. En el cliente, generalmente solo necesita un par de esos.

Hacer una llamada REST en el punto final /users le devuelve todos los campos del usuario, y el cliente solo usa los que necesita. Claramente, se desperdicia algo en la transferencia de datos, lo que podría ser una consideración en los clientes móviles.

GraphQL por defecto obtiene los datos más pequeños posibles. Si solo necesita el nombre y apellido de sus usuarios, especifíquelo en su consulta.

La siguiente interfaz se llama GraphiQL, que es como un explorador de API para GraphQL. Creé un pequeño proyecto para el propósito de este artículo. El código está alojado en GitHub y lo analizaremos en la segunda parte.

En el panel izquierdo de la interfaz está la consulta. Aquí, estamos recuperando a todos los usuarios, haríamos GET /users con REST, y solo obtendríamos su nombre y apellido.

Consulta

 query { users { firstname lastname } }

Resultado

 { "data": { "users": [ { "firstname": "John", "lastname": "Doe" }, { "firstname": "Alicia", "lastname": "Smith" } ] } }

Si también quisiéramos obtener los correos electrónicos, agregar una línea de "correo electrónico" debajo de "apellido" sería suficiente.

Algunos back-end REST ofrecen opciones como /users?fields=firstname,lastname para devolver recursos parciales. Por lo que vale, Google lo recomienda. Sin embargo, no se implementa de forma predeterminada y hace que la solicitud sea apenas legible, especialmente cuando agrega otros parámetros de consulta:

  • &status=active para filtrar usuarios activos
  • &sort=createdAat para ordenar los usuarios por su fecha de creación
  • &sortDirection=desc porque obviamente lo necesitas
  • &include=projects para incluir los proyectos de los usuarios

Estos parámetros de consulta son parches agregados a la API REST para imitar un lenguaje de consulta. GraphQL es sobre todo un lenguaje de consulta, que hace que las solicitudes sean concisas y precisas desde el principio.

Razón 2: la opción de diseño "Incluir vs. Endpoint"

Imaginemos que queremos construir una herramienta simple de gestión de proyectos. Tenemos tres recursos: usuarios, proyectos y tareas. También definimos las siguientes relaciones entre los recursos:

Relaciones entre recursos

Estos son algunos de los puntos finales que exponemos al mundo:

punto final Descripción
GET /users Listar todos los usuarios
GET /users/:id Obtenga el usuario único con id: id
GET /users/:id/projects Obtener todos los proyectos de un usuario

Los puntos finales son simples, fáciles de leer y están bien organizados.

Las cosas se complican cuando nuestras solicitudes se vuelven más complejas. Tomemos el punto final GET /users/:id/projects : supongamos que quiero mostrar solo los títulos de los proyectos en la página de inicio, pero los proyectos+tareas en el tablero, sin realizar varias llamadas REST. Yo llamaría:

  • GET /users/:id/projects para la página de inicio.
  • GET /users/:id/projects?include=tasks (por ejemplo) en la página del tablero para que el back-end agregue todas las tareas relacionadas.

Es una práctica común agregar parámetros de consulta ?include=... para que esto funcione, e incluso lo recomienda la especificación de la API de JSON. Los parámetros de consulta como ?include=tasks todavía se pueden leer, pero en poco tiempo terminaremos con ?include=tasks,tasks.owner,tasks.comments,tasks.comments.author .

En este caso, ¿sería más inteligente crear un punto final /projects para hacer esto? ¿Algo así como /projects?userId=:id&include=tasks , ya que tendríamos un nivel de relación menos para incluir? O, en realidad, un punto final /tasks?userId=:id podría funcionar también. Esta puede ser una elección de diseño difícil, incluso más complicada si tenemos una relación de muchos a muchos.

GraphQL utiliza el enfoque de include en todas partes. Esto hace que la sintaxis para buscar relaciones sea poderosa y consistente.

Aquí hay un ejemplo de cómo obtener todos los proyectos y tareas del usuario con id 1.

Consulta

 { user(id: 1) { projects { name tasks { description } } } }

Resultado

 { "data": { "user": { "projects": [ { "name": "Migrate from REST to GraphQL", "tasks": [ { "description": "Read tutorial" }, { "description": "Start coding" } ] }, { "name": "Create a blog", "tasks": [ { "description": "Write draft of article" }, { "description": "Set up blog platform" } ] } ] } } }

Como puede ver, la sintaxis de consulta es fácilmente legible. Si quisiéramos profundizar e incluir tareas, comentarios, imágenes y autores, no lo pensaríamos dos veces en cómo organizar nuestra API. GraphQL facilita la obtención de objetos complejos.

Razón 3: Gestión de diferentes tipos de clientes

Cuando construimos un back-end, siempre comenzamos tratando de hacer que la API sea lo más ampliamente posible para todos los clientes. Sin embargo, los clientes siempre quieren llamar menos y obtener más. Con inclusiones profundas, recursos parciales y filtrado, las solicitudes realizadas por clientes web y móviles pueden diferir mucho entre sí.

Con REST, hay un par de soluciones. Podemos crear un punto final personalizado (es decir, un punto final de alias, por ejemplo, /mobile_user ), una representación personalizada ( Content-Type: application/vnd.rest-app-example.com+v1+mobile+json ), o incluso un cliente -API específica (como lo hizo alguna vez Netflix). Los tres requieren un esfuerzo adicional por parte del equipo de desarrollo de back-end.

GraphQL da más poder al cliente. Si el cliente necesita solicitudes complejas, él mismo construirá las consultas correspondientes. Cada cliente puede consumir la misma API de manera diferente.

Cómo empezar con GraphQL

En la mayoría de los debates sobre "GraphQL vs. REST" hoy en día, las personas piensan que deben elegir cualquiera de los dos. Esto simplemente no es cierto.

Las aplicaciones modernas generalmente usan varios servicios diferentes, que exponen varias API. De hecho, podríamos pensar en GraphQL como una puerta de enlace o un envoltorio para todos estos servicios. Todos los clientes llegarían al punto final de GraphQL, y este punto final llegaría a la capa de la base de datos, un servicio externo como ElasticSearch o Sendgrid, u otros puntos finales REST.

Comparaciones de puntos finales de GraphQL frente a REST

Una segunda forma de usar ambos es tener un punto final /graphql separado en su API REST. Esto es especialmente útil si ya tiene numerosos clientes accediendo a su API REST, pero desea probar GraphQL sin comprometer la infraestructura existente. Y esta es la solución que estamos explorando hoy.

Como dije anteriormente, ilustraré este tutorial con un pequeño proyecto de ejemplo, disponible en GitHub. Es una herramienta de gestión de proyectos simplificada, con usuarios, proyectos y tareas.

Las tecnologías utilizadas para este proyecto son Node.js y Express para el servidor web, SQLite como base de datos relacional y Sequelize como ORM. Los tres modelos (usuario, proyecto y tarea) se definen en la carpeta de models . Los extremos de REST /api/users , /api/projects y /api/tasks están expuestos al mundo y se definen en la carpeta de rest .

Tenga en cuenta que GraphQL se puede instalar en cualquier tipo de back-end y base de datos, utilizando cualquier lenguaje de programación. Las tecnologías utilizadas aquí se eligen en aras de la simplicidad y la legibilidad.

Nuestro objetivo es crear un punto final /graphql sin eliminar los puntos finales REST. El punto final de GraphQL llegará directamente al ORM de la base de datos para obtener datos, de modo que sea totalmente independiente de la lógica REST.

Tipos

El modelo de datos está representado en GraphQL por tipos , que están fuertemente tipados. Debería haber un mapeo 1 a 1 entre sus modelos y los tipos de GraphQL. Nuestro tipo User sería:

 type User { id: ID! # The "!" means required firstname: String lastname: String email: String projects: [Project] # Project is another GraphQL type }

Consultas

Las consultas definen qué consultas puede ejecutar en su API de GraphQL. Por convención, debe haber un RootQuery que contenga todas las consultas existentes. También señalé el equivalente REST de cada consulta:

 type RootQuery { user(id: ID): User # Corresponds to GET /api/users/:id users: [User] # Corresponds to GET /api/users project(id: ID!): Project # Corresponds to GET /api/projects/:id projects: [Project] # Corresponds to GET /api/projects task(id: ID!): Task # Corresponds to GET /api/tasks/:id tasks: [Task] # Corresponds to GET /api/tasks }

Mutaciones

Si las consultas son solicitudes GET , las mutaciones pueden verse como POST / PATCH / PUT / DELETE (aunque en realidad son versiones sincronizadas de consultas).

Por convención, ponemos todas nuestras mutaciones en RootMutation :

 type RootMutation { createUser(input: UserInput!): User # Corresponds to POST /api/users updateUser(id: ID!, input: UserInput!): User # Corresponds to PATCH /api/users removeUser(id: ID!): User # Corresponds to DELETE /api/users createProject(input: ProjectInput!): Project updateProject(id: ID!, input: ProjectInput!): Project removeProject(id: ID!): Project createTask(input: TaskInput!): Task updateTask(id: ID!, input: TaskInput!): Task removeTask(id: ID!): Task }

Tenga en cuenta que presentamos nuevos tipos aquí, llamados UserInput , ProjectInput y TaskInput . Esta es una práctica común con REST también, para crear un modelo de datos de entrada para crear y actualizar recursos. Aquí, nuestro tipo de UserInput de usuario es nuestro tipo User sin los campos de id y projects , y observe la input de palabra clave en lugar de type :

 input UserInput { firstname: String lastname: String email: String }

Esquema

Con tipos, consultas y mutaciones, definimos el esquema de GraphQL , que es lo que el extremo de GraphQL expone al mundo:

 schema { query: RootQuery mutation: RootMutation }

Este esquema está fuertemente tipado y es lo que nos permitió tener esos prácticos autocompletados en GraphiQL.

Resolutores

Ahora que tenemos el esquema público, es hora de decirle a GraphQL qué hacer cuando se solicita cada una de estas consultas/mutaciones. Los solucionadores hacen el trabajo duro; pueden, por ejemplo:

  • Llegar a un punto final REST interno
  • Llamar a un microservicio
  • Acceda a la capa de la base de datos para realizar operaciones CRUD

Estamos eligiendo la tercera opción en nuestra aplicación de ejemplo. Echemos un vistazo a nuestro archivo de resolución:

 const models = sequelize.models; RootQuery: { user (root, { id }, context) { return models.User.findById(id, context); }, users (root, args, context) { return models.User.findAll({}, context); }, // Resolvers for Project and Task go here }, /* For reminder, our RootQuery type was: type RootQuery { user(id: ID): User users: [User] # Other queries }

Esto significa que, si se solicita la consulta del user(id: ID!) en GraphQL, devolvemos User.findById() , que es una función ORM Sequelize, desde la base de datos.

¿Qué hay de unir otros modelos en la solicitud? Bueno, necesitamos definir más resolutores:

 User: { projects (user) { return user.getProjects(); // getProjects is a function managed by Sequelize ORM } }, /* For reminder, our User type was: type User { projects: [Project] # We defined a resolver above for this field # ...other fields } */

Entonces, cuando solicitamos el campo de projects en un tipo de User en GraphQL, esta combinación se agregará a la consulta de la base de datos.

Y finalmente, resolutores para mutaciones:

 RootMutation: { createUser (root, { input }, context) { return models.User.create(input, context); }, updateUser (root, { id, input }, context) { return models.User.update(input, { ...context, where: { id } }); }, removeUser (root, { id }, context) { return models.User.destroy(input, { ...context, where: { id } }); }, // ... Resolvers for Project and Task go here }

Puedes jugar con esto aquí. En aras de mantener limpios los datos en el servidor, deshabilité los resolutores para las mutaciones, lo que significa que las mutaciones no realizarán ninguna operación de creación, actualización o eliminación en la base de datos (y, por lo tanto, devolverán un null en la interfaz).

Consulta

 query getUserWithProjects { user(id: 2) { firstname lastname projects { name tasks { description } } } } mutation createProject { createProject(input: {name: "New Project", UserId: 2}) { id name } }

Resultado

 { "data": { "user": { "firstname": "Alicia", "lastname": "Smith", "projects": [ { "name": "Email Marketing Campaign", "tasks": [ { "description": "Get list of users" }, { "description": "Write email template" } ] }, { "name": "Hire new developer", "tasks": [ { "description": "Find candidates" }, { "description": "Prepare interview" } ] } ] } } }

Puede tomar algún tiempo reescribir todos los tipos, consultas y resoluciones para su aplicación existente. Sin embargo, existen muchas herramientas para ayudarte. Por ejemplo, hay herramientas que traducen un esquema SQL a un esquema GraphQL, ¡incluidos los solucionadores!

Poniendo todo junto

Con un esquema bien definido y resoluciones sobre qué hacer en cada consulta del esquema, podemos montar un punto final /graphql en nuestro back-end:

 // Mount GraphQL on /graphql const schema = makeExecutableSchema({ typeDefs, // Our RootQuery and RootMutation schema resolvers: resolvers() // Our resolvers }); app.use('/graphql', graphqlExpress({ schema }));

Y podemos tener una interfaz GraphiQL atractiva en nuestro back-end. Para realizar una solicitud sin GraphiQL, simplemente copie la URL de la solicitud y ejecútela con cURL, AJAX o directamente en el navegador. Por supuesto, hay algunos clientes de GraphQL para ayudarlo a crear estas consultas. Vea a continuación algunos ejemplos.

¿Que sigue?

El objetivo de este artículo es darle una idea de cómo se ve GraphQL y mostrarle que definitivamente es posible probar GraphQL sin desechar su infraestructura REST. La mejor manera de saber si GraphQL se adapta a tus necesidades es probarlo tú mismo. Espero que este artículo te haga sumergirte.

Hay muchas funciones de las que no hemos hablado en este artículo, como actualizaciones en tiempo real, procesamiento por lotes del lado del servidor, autenticación, autorización, almacenamiento en caché del lado del cliente, carga de archivos, etc. Un excelente recurso para conocer estas funciones. es Cómo GraphQL.

A continuación se presentan algunos otros recursos útiles:

Herramienta del lado del servidor Descripción
graphql-js La implementación de referencia de GraphQL. Puede usarlo con express-graphql para crear un servidor.
graphql-server Un servidor GraphQL todo en uno creado por el equipo de Apollo.
Implementaciones para otras plataformas Rubí, PHP, etc.
Herramienta del lado del cliente Descripción
Relé Un marco para conectar React con GraphQL.
apolo-cliente. Un cliente GraphQL con enlaces para React, Angular 2 y otros marcos front-end.

En conclusión, creo que GraphQL es más que exageración. Todavía no reemplazará a REST mañana, pero ofrece una solución eficaz para un problema real. Es relativamente nuevo y las mejores prácticas aún se están desarrollando, pero definitivamente es una tecnología de la que oiremos hablar en los próximos años.