Diagrama obtenido de la documentación de Hasura
En este diagrama puedes encontrar una descripción de la comunicación entre un cliente y servidor de GraphQL.
query
) que describe la información que se quiere obtener.endpoint
(usualmente /graphql
)POST
al endpoint provisto por el servidor enviando la consulta definida como un string JSON.GraphQL permite el uso de "declarative data fetching" (obtención de datos declarativa), es decir, el cliente es quien define y específica que datos necesita exactamente.
A modo de comparación, en el caso de una API tipo REST, el servidor expone múltiples endpoint para cada uno de los recursos expuestos y es el servidor quien define que datos se retornan en cada URL.
Esta idea de permitir al cliente definir los datos requeridos permite evitar algunos problemas de "sobre consulta" existentes en la arquitectura REST en donde muchas veces un endpoint retorna una excesiva cantidad de datos, creando dificultades en asegurar que el cliente está recibiendo el set de datos correcto.
Otro problema que GraphQL intenta resolver es el consultar diferentes recursos o entidades relacionadas, en el caso de REST esto implica consultar diferentes endpoints, en GraphQL todas las consultas son enviadas a un mismo endpoint provisto por el servidor.
Hay una serie de características claves en el diseño de GraphQL: Declarativo, Jerárquico, Introspectivo y fuertemente tipado.
Las consultas en GraphQL son declarativas, es decir, el cliente declara exactamente que campos o atributos necesita. La respuesta recibida sólo incluirá dichas propiedades.
{
user(id: "1") {
name
address
email
}
}
El código anterior muestra una consulta que permite obtener un user
identificado por el id
"1". Esta consulta solicita los campos name
, address
e email
.
La posible respuesta a esta consulta será un objeto en formato JSON cuyo atributo data
será un objeto que contiene los campos solicitados.
{
"data": {
"user": {
"name":"Matías",
"address":"Some place",
"email":"hola@matiashernandez.dev"
}
}
}
Las consultas en GraphQL son jerárquicas. Los datos retornados siguen la misma forma o definición de la consulta.
{
user(id: "1") {
name
address
email
networks {
name
url
}
}
}
La consulta anterior ha sido extendidad para incluir el campo networks
que a su vez solicita los campos name
y url
.
{
"data": {
"user": {
"name":"Matías",
"address":"Some place",
"email":"hola@matiashernandez.dev",
"networks" [
{
"name": "twitter",
"url:" "<https://twitter.com/matiasfha>",
},
{
"name":"Polywork",
"url":"<https://tl.matiashernandez.dev>"
}
]
}
}
}
La respuesta ahora incluye un arreglo con todos los valores asociados a la propiedad networks
de este user
en particular. GraphQL no tiene opinión ni fuerza una forma de almacenamiento de datos en particular, por lo que es incluso posible que users
y networks
estén almacenados en distintas bases de datos. Si esto fuese así, es el servidor que implementa la especificación GraphQL y expone el endpoint quien debe ocuparse de recolectar los datos (implementación de resolvers
).
La Introspección es una característica que permite que clientes puedan obtener el esquema que da forma a los datos utilizados por ese endpoint GraphQL. Esto permite la creación de herramientas como GraphQL, un playground para el servidor que permite ejecutar consultas, pero por sobre todo acceder a la documentación existente.
Por medio de esta característica es posible conocer que otras propiedades pueden ser consultadas para el campo networks
sin la necesidad de mirar el código del servidor.
{
_schema {
types {
name
kind
description
}
}
}
Esta consulta retornara la descripción del esquema, algo similar a:
{
"data": {
"__schema": {
"types": [
{
"name": "Network",
"kind": "OBJECT",
"description": "A description of a social network"
}
]
}
}
}
La especificación describe un sistema de tipos lo que permite definir las capacidades que cada valor o campo tendrá dentro del servidor GraphQL.
Los tipos utilizados dentro del esquema son muy similares a los tipos encontrados en Typescript y otros lenguajes, incluyendo primitivas como String
,Boolean
, Int
y tipos más avanzados.
type Network {
name: String!
url: String!
priority: Int
}
Este ejemplo define un tipo objeto llamado Network
, que se compone de dos campos requeridos name
y url
, ambos del tipo String
y un campo opcional priority
de tipo Int
.
El esquema de GraphQL es definido utilizando el sistema de tipos, lo que permite al servidor determinar si una consulta es o no válida antes de intentar ejecutarla.
Este sistema de tipos permite asegurar que las consultas son sintéticamente correctas, evitando así ambigüedades y errores.
Las implementaciones de GraphQL constan de dos partes esenciales:
query
) con su correspondiente set de datos (resolvers
).Una API GraphQL es definida usualmente con un solo endpoint, por lo general una URL que termina en /graphql
. Por medio de esta URL es posible acceder a todas las consultas y mutaciones ofrecidas por el servidor, incluyendo la introspección o soluciones como GraphQL (si es ofrecida).
Dado que GraphQL es una especificación totalmente agnóstica de la tecnología subyacente o de el "medio de transporte" utilizado, el servidor puede ser desarrollado con tu stack preferido o incluso utilizando protocolos de comunicación diferentes como RPC, sin embargo, lo más común es que el endpoit sea servido mediante HTTP
.
Existen muchas implementaciones de servidores GraphQL, en el mundo de Node.js puedes encontrar varias alternativas como por ejemplo:
Puedes encontrar más implementaciones en este sitio web
El servidor se encargará de escuchar las peticiones realizadas por los clientes y convertir o resolver dichas consultas comunicándose con la capa de datos correspondiente.
GraphQL es completamente agnóstico y sin opinión sobre que base de datos debes utilizar, incluso es posible que puedas utilizar múltiples soluciones para almacenar y consultar tus datos, permitiéndote realizar agregación de datos que luego serán provistos como respuesta a la consulta por medio del único endpoint.
Lo importante es definir correctamente el esquema que declarará cuál es la API de los datos disponibles a ser consultados.
Las consultas (y mutaciones) realizadas hacia el servidor GraphQL son llamadas documents
que tienen cierto formato definido por la especificación.
Existen varios clientes que permiten realizar algunas operaciones avanzadas como manejo de Caché, reiteración en caso de error, etc. También puedes hacerlo con "vainilla" JavaScript, ya que estos documentos no son más que un string JSON.
{
"query": "query whoAmI {\\n whoAmI {\\n userId\\n firstName\\n lastName\\n }\\n }"
}
Esta consulta puede ser fácilmente implementada utilizando fetch
.
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `query whoAmI {
whoAmI {
userId
firstName
lastName
}
}
`
}),
})
const result = await response.json()
Algunos clientes javascript que puedes encontrar:
fetch
.Encuentra más soluciones en este enlace
Esta pregunta tipo "versus" es muy común, pero era en un punto. GraphQL y REST no son conceptos intercambiables.
Sí, es cierto que ambos conceptos buscan resolver problemas similares, pero son ideas diferentes.
REST son las siglas de Representational State Transfer. Es una arquitectura de software que permite definir como se comparten datos entre sistemas. Una API REST es una API que implementa los principios y restricciones definidas por REST, tales como: No maneja estado, es "cacheable", separa las tareas o intereses del cliente y el servidor, mantiene una interfaz uniforme.
GraphQL, es la especificación de un lenguaje de consulta.
Como todo en el mundo del desarrollo web, ambas ideas, conceptos e implementaciones tiene puntos a favor y en contra y ambos tienen usos en el día de hoy.
Quizá la principal ventaja de GraphQL sobre REST es que mantiene un solo punto de entrada, permitiendo (o forzando) agregar o recolectar la información necesaria - que incluso podría provenir de varias API REST - para ser servida tal y como el cliente la solicitó.
GraphQL es un restaurante a la carta. REST es un comedor en donde el chef define que comerás.
Esto, resuelve el problema de over-fetching
y under-fetching
.
Además, con GraphQL puedes:
Puedes desarrollar tu capa de abstracción con GraphQL tanto para consumir una API GraphQL (cliente) o creando tu propia API (server).
En este breve ejemplo crearás:
Puedes encontrar el código fuente de este ejemplo en este repositorio.
Comencemos por el servidor, primero, crea un directorio y dentro de el dos directorios más
mkdir graphql-demo
cd graphql-demo
mkdir servidor client
Ahora dentro del servidor, crearemos un nuevo proyecto, instalar las dependencias y escribir el código.
npm init -y
npm install cors @faker-js/faker @graphql-tools/graphql-file-loader @graphql-tools/load @graphql-tools/schema express express-graphql graphql
Ahora actualiza el archivo package.json
y agrega:
"scripts" {
"start": "nodemon server.js"
},
"type": "module"
Ahora es tiempo de constuir el schema
para luego crear los resolvers
para cada query.
El esquema lo crearemos directamente escribiendo graphql, para eso: crea un directorio llamado graphql
y dentro de el un archivo index.graphql
type Article {
title: String
author: User!
slug: String!
content: String!
}
type User {
name: String!
email: String!
articles: [Article]
id: String!
}
type Query {
"""
Obtiene una lista de usuarios
"""
users: [User]
"""
Retorna un Usuario identificado por id
"""
user(id: String!): User!
"""
Obtiene una lista de articulos
"""
articles: [Article]
}
En este archivo defines los tipos de tus datos o entidades, además de cuáles serán los nombres para las consultas y los datos retornados.
Aquí has creado dos tipos: Article
y User
. Cada uno define sus propios atributos indicando cuáles son requeridos al utilizar el símbolo !
.
Además, se definen las consultas disponibles dentro del tipo objeto Query
. En este caso, tu API podrás permitir consultas para users
, user
y articles
.
Ahora, definiremos los resolvers. Dado que este un demo pequeño, puedes escribir los resolvers directamente en el archivo principal.
Crea un archivo llamado server.js
y dentro de él crea un objeto llamado resolvers
.
Este objeto tiene la misma estructura que el tipo Query
definido anteriormente.
const resolvers = {
users: () => null,
user: (obj, args) => null,
articles: () => null
}
Puedes asumir que los datos provienen de alguna API externa o base de datos, para este caso usaremos fakerjs para crear datos de prueba.
Crea un archivo llamado data.js
esta será la forma de emular una API o DB.
// Mock data
import {faker} from '@faker-js/faker'
const userCreator = () => ({
name: faker.name.firstName(),
email: faker.internet.email(),
id: faker.datatype.uuid()
});
const users = [...new Array(7)].map(() => userCreator())
const articleCreator = () => ({
title: faker.lorem.words(5),
slug: faker.lorem.slug(),
content: faker.lorem.paragraphs(),
author: faker.helpers.arrayElements(users, 1)[0] // Relacion con User
})
const articles = [...new Array(7)].map(() => articleCreator())
export {
articles,
users
}
Este archivo simplemente crea un arreglo de usuarios y otro de artículos con datos ficticios.
De vuelta al archivo server.js
Es hora de importar los datos ficticios y definir los resolvers
.
/* Resolvers */
import { articles, users } from './data.js'; //fake data
const resolvers = {
Query: {
users: () => {
return users.map(u => {
// Encontrar los articulos del usuario
return {
...u,
articles: articles.filter(a => {
return a.author.id === u.id
})
}
})
},
user: (obj, args) => {
if(args.userId) {
const user = users.find(u => u.id === userId)
return {
...user,
articles: articles.find(a => a.author.id === args.userId)
}
}
return null;
},
articles: () => articles
}
}
Importante notar que la función user
recibe dos argumentos. Estos argumentos están definidos por graphql desde la definición de la query, ya que esta query
contiene argumentos.
Ahora, utilizando una de las dependencias instaladas, definires el schema
e iniciaremos el servidor (mismo archivo)
import express from 'express'
import cors from 'cors'
import { graphqlHTTP } from 'express-graphql'
import { loadSchemaSync } from '@graphql-tools/load'
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'
import { addResolversToSchema } from '@graphql-tools/schema'
// Carga la definición de tipos
const typeDefs = loadSchemaSync('./graphql/index.graphql', {
loaders: [new GraphQLFileLoader()]
});
// Asocia los tipos con loss resolvers creando el schema
const schema = addResolversToSchema({
schema: typeDefs,
resolvers,
})
const app = express();
app.use(cors()) // Nos aseguramos de poder acceder al servidor
// Añade el servicio graphql y graphiql
app.use('/graphql', graphqlHTTP({
schema,
graphiql: true
}))
app.listen(4000)
console.log('Tu servidor GraphQL esta corriendo en <http://localhost:4000/graphql>')
Con esto en su lugar, ya puedes iniciar tu servidor con
npm run start
y visitar
http://localhost:4000/graphql
en donde verás el client gráfico y documentación.
Para el cliente, creamos una pequeña app React utilizando vitejs.
Para eso, dentro de la raíz de tu proyecto ejecuta
npm create vite@latest
Sigue las instrucciones y seleccion react
.
Ahora, agregarás algunas dependencias para poder comunicarte con tu servidor graphql.
npm install graphql graphql-request
npm install
Ahora ya puedes editar el código de esta pequeña app. Abre src/App.jsx
. Puedes eliminar su contenido.
Dado que este es un pequeño demo, definiremos los componentes directamente en este archivo.
Primero, definir como se consultarán los datos.
import { gql, GraphQLClient } from 'graphql-request'
const usersQuery = gql`
query users {
users {
id
name
email
articles {
title
slug
}
}
}
`
const articlesQuery = gql`
query articles {
articles {
title
author {
name
}
slug
}
}
`
const client = new GraphQLClient('<http://localhost:4000/graphql>', { headers: {} })
async function requestUsers() {
try {
const data = await client.request(usersQuery)
return data.users;
}catch(e){
console.error(e);
return []
}
}
async function requestArticles() {
try {
const data = await client.request(articlesQuery)
return data.articles;
}catch(e){
console.error(e);
return []
}
}
Con este trozo de código (que nada tiene que ver con React y puede ser utilizado en cualquier framework). Has definido:
gql
para poder escribir directamente en graphqlGraphqlClient
Ahora, creemos un componente para desplegar los usuarios.
import { gql, GraphQLClient } from 'graphql-request'
const usersQuery = gql`
query users {
users {
id
name
email
articles {
title
slug
}
}
}
`
const articlesQuery = gql`
query articles {
articles {
title
author {
name
}
slug
}
}
`
const client = new GraphQLClient('<http://localhost:4000/graphql>', { headers: {} })
async function requestUsers() {
try {
const data = await client.request(usersQuery)
return data.users;
}catch(e){
console.error(e);
return []
}
}
async function requestArticles() {
try {
const data = await client.request(articlesQuery)
return data.articles;
}catch(e){
console.error(e);
return []
}
}
Este componente es una simple tabla que despliega los usuarios obtenidos utilizando requestUsers
.
Usando useEffect
ejecuta la llamada al servidor graphql al momento de renderizar el componente y almacena esos datos en el estado utilizando useState
.
También puedes consultar datos reaccionando a un evento de usuario como en este componente para desplegar artículos.
const ArticlesTable = () => {
const [articles, setArticles] = useState(null)
const fetchArticles = async () => {
const data = await requestArticles()
setArticles(data)
}
return articles!==null ? (
<>
<h2>Articles</h2>
<table>
<thead>
<tr>
<th>Titulo</th>
<th>Autor</th>
<th>Slug</th>
</tr>
</thead>
<tbody>
{articles.map(article =>(
<tr>
<td>{article.title}</td>
<td>{article.author.name}</td>
<td>{article.slug}</td>
</tr>
))}
</tbody>
</table>
</>
):<button onClick={fetchArticles}>Fetch Articles</button>
}
Muy similar al anterior componente pero esta vez se renderiza un botón que al ser clickeado ejecuta una función para obtener los datos.
Finalmente, renderiza los componentes
function App() {
return (
<div className="App">
<h1>Graphql Demo</h1>
<UsersTable />
<hr />
<ArticlesTable />
</div>
)
}
export default App
Puedes ver todo en funcionamiento al abrir dos terminales y en una ejecutar el servidor y en otra el cliente
npm run start # Servidor
npm run dev # Client
GraphQL es la especificación de un lenguaje de consulta y un "runtime" mantenida por la comunidad open-source. Busca resolver varios problemas encontrados en las ya tradicionales implementaciones de REST como over/under fetching
, uso ineficiente de las llamadas de red, etc.
Sin embargo GraphQL no es un reemplazo directo de REST dado que son conceptos diferentes e incluso pueden convivir en una misma implementación.