NestJS: API GraphQL desde 0
Claves para poder crear una API GraphQL sencilla con NestJS
Comenzamos con un nuevo artículo en el que se explicará cómo crear una API GraphQL desde cero utilizando NestJS, un framework de Node.js que permite crear aplicaciones escalables y eficientes en el lado del servidor.
Se creará una API muy sencilla mediante el enfoque para la creación de la API Schema First
y se mostrarán las claves para poder crear una API GraphQL sencilla con NestJS explicando paso a paso todo lo que se irá haciendo.
Contenido que vamos a encontrar
- Instalar y crear proyecto NestJS.
- Instalar dependencias necesarias para trabajar en el proyecto.
- Configurar módulo para implementar GraphQL.
- Primeros pasos definiendo nuestro Schema y configuración.
- Implementar las definiciones en Typescript
- Resolvers.
- Importar el provider GraphQLResolver
- Probando la aplicación.
- Conclusión.
Instalar CLI y crear proyecto NestJS
- Abrimos el terminal terminal y ejecutamos el siguiente comando:
npm i -g @nestjs/cli
(En estos momento trabajo con la versión 9 de NestJS y todo estará relacionado con esta versión a continuación) - Después, crearemos un nuevo proyecto NestJS:
nest new nombre-de-tu-proyecto
Como propuesta, podemos poner por añadir un nombre descriptivo:
nest new nest-api-graphql
Lo primero nos preguntan que gestor de paquetes utilizaremos y en este caso os recomiendo que uséis el que más cómodo os resulte.
Yo por simplificar y viendo que el más conocido es npm, seleccionaré este primero:
Con esto, esperamos a que se ejecute la instalación y que se complete todo el proceso y nos aparecerá un mensaje similar al siguiente, dándonos el OK para seguir hacia adelante:
Instalar las dependencias necesarias para GraphQL
- Ejecuta los siguientes comandos:
npm i --save @nestjs/graphql@11 graphql-tools@8 graphql@16 apollo-server-express@3 @nestjs/apollo@11
npm i --save-dev @types/graphql@14 @nestjs/testing@9 ts-morph@17
Configurar módulo para implementar GraphQL
Una vez que los paquetes están instalados, podemos importar GraphQLModule
y configurarlo con el método estático forRoot()
.
En la carpeta src
vamos al archivo llamado app.module.ts
y añadimos el código pasando de lo siguiente:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
A esto, que se implementará de la siguiente manera:
// Quitamos el controlador y servicio (app) para simplificar el proceso
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
}),
]
})
export class AppModule {}
El método forRoot()
toma un objeto de opciones como argumento.
Estas opciones se pasan a la instancia del controlador subyacente (Más información complementaria de las opciones que existen: Apollo ). No nos vamos a centrar en estas opciones, no es el propósito del artículo,
Siguiendo con lo que estamos trabajando, NestJS nos ofrece dos formas de construir aplicaciones GraphQL: el enfoque de código primero (Code First
) y el enfoque de esquema primero (Schema First
) .
En este artículo seguiré adelante con el enfoque de Schema First
, próximamente en un nuevo artículo haré el mismo ejemplo que el que voy a implementar pero haciendo uso del enfoque Code First
Primeros pasos definiendo nuestro Schema y configuración
En el enfoque de esquema primero, la fuente de verdad son los archivos SDL (Schema Definition Language)
de GraphQL.
SDL
es una forma independiente del lenguaje para compartir archivos de esquema entre diferentes plataformas.
NestJS genera automáticamente sus definiciones de TypeScript (usando clases o interfaces) basadas en los esquemas GraphQL para reducir la necesidad de escribir código redundante .
Para empezar con el enfoque del Schema First, creamos un fichero con extensión .graphql
en nuestro proyecto, por ejemplo en src/graphql/schemas/schema.graphql
y definimos el contenido de nuestro esquema allí.
Luego, debemos de configurar el GraphQLModule
para que apunte a este archivo y genere automáticamente las definiciones de TypeScript para nuestro proyecto.
Todo lo que vamos a implementar, nos basaremos en la información de la documentación oficial, que es recomendable tener a mano para futuros cambios, si se diesen poder saber que hay que configurar de nuevo y como hacerlo
Aquí un ejemplo paso a paso de cómo podemos hacer esto, siguiendo lo indicado:
- Creamos un archivo
schema.graphql
en su proyecto (src/schemas/schema.graphql
) y definimos nuestro esquema:
# Operaciones de consulta
type Query {
hello: String
}
# Operaciones de modificación tanto Crear, Actualizar y eliminar
type Mutation {
add(input: NumbersInput): Float
}
# Elemento que se usa para simpificar la entrada de una definición
# Como se puede ver, se usa en la definición "add" para introducir dos números
input NumbersInput {
a: Float
b: Float
}
- Configuramos la configuración de
GraphQLModule
para que apunte a este archivo ( y todos los que se creen con esta extensión):
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
@Module({
imports: [
GraphQLModule.forRoot({
driver: ApolloDriver,
typePaths: ['./**/*.graphql'], // <========= ESTO COGE TODOS LOS .graphql
}),
],
})
export class AppModule {}
Con esto, NestJS generará automáticamente las definiciones de TypeScript para nosotros y podremos empezar a implementar sus resolvers para dar solución a lo que hemos definido como nuestra definiciones..
Este archivo definirá el esquema GraphQL para tu API y será el contrato que nos una para proporcionar las opciones que tendrá esa API.
Si queréis aprender las bases teórico — prácticas de GraphQL, os dejo un curso totalmente gratuito en el que os enseño a trabajar con el desde 0 enseñando todos los conceptos fundamentales como los typeRoots (Query, Mutation, Subscription), uso de Input, Interfaces,…
Esto todo lo podéis encontrar en el siguiente enlace:
https://www.udemy.com/course/introduccion-a-graphql-desde-las-bases-hasta-crear-apis/
Os recomiendo que veáis sobre todo estos capítulos:
Ahí aprenderemos las nociones básicas, como construir nuestro esquema, sabiendo lo que hacemos y posteriormente como consumir esa API en el playground.
Implementar las definiciones en Typescript
Por lo general, también necesitaremos tener definiciones de TypeScript (clases e interfaces) que correspondan a los tipos GraphQL SDL.
Crear las definiciones de TypeScript correspondientes a mano es redundante y tedioso, por lo que vamos a implementar la opción de generar estos elementos de manera automática ya que si no lo hacemos así, por cada cambio mínimo realizado dentro de SDL nos obligaría a ajustar también las definiciones de TypeScript y eso no es muy práctico en consecuencia será una cantidad ingente de futuros errores “tontos”.
Para abordar esto, el paquete @nestjs/graphql nos permite generar automáticamente definiciones de TypeScript a partir del árbol de sintaxis abstracta (AST).
Para habilitar esta función, debemos de agregar la propiedad de opciones de definiciones al configurar GraphQLModule.
Pasamos de lo siguiente
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
typePaths: ['./**/*.graphql'],
// <============= AQUÍ
}),
Añadiendo esto:
GraphQLModule.forRoot<ApolloDriverConfig>({
...
definitions: {
path: join(process.cwd(), 'src/graphql.ts'),
},
}),
Quedaría de la siguiente manera:
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
typePaths: ['./**/*.graphql'],
definitions: {
path: join(process.cwd(), 'src/graphql.ts'),
},
}),
La propiedad de ruta del objeto de definiciones indicada es la ruta para guardar la salida de TypeScript generada.
De forma predeterminada, todos los tipos de TypeScript generados se crean como interfaces. Para generar clases en su lugar, especifique la propiedad outputAs
con un valor de class
.
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
typePaths: ['./**/*.graphql'],
definitions: {
path: join(process.cwd(), 'src/graphql.ts'),
outputAs: 'class', <====
},
}),
Yo seguiré con la opción predeterminada, podéis usar la generación de clases, lo que deseéis.
Ahora en la raíz del proyecto creamos un nuevo fichero llamado generate-typings.ts
añadiendo el siguiente contenido:
import { GraphQLDefinitionsFactory } from '@nestjs/graphql';
import { join } from 'path';
const definitionsFactory = new GraphQLDefinitionsFactory();
definitionsFactory.generate({
typePaths: ['./src/**/*.graphql'],
path: join(process.cwd(), 'src/graphql.ts')
});
Una vez que tengamos esto, añadimos un nuevo comando en el apartado de scripts de nuestro fichero package.json
"generate:typings": "npx ts-node generate-typings"
Y ejecutando ese script:
Con esto, se nos tiene que crear un nuevo fichero llamado graphql.ts
dentro de src
Cuyo contenido para el schema creado es:
/*
* -------------------------------------------------------
* THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
* -------------------------------------------------------
*/
/* tslint:disable */
/* eslint-disable */
export interface NumbersInput {
a?: Nullable<number>;
b?: Nullable<number>;
}
export interface IQuery {
hello(): string | Promise<string>;
}
export interface IMutation {
add(input?: Nullable<NumbersInput>): Nullable<number> | Promise<Nullable<number>>;
}
type Nullable<T> = T | null;
Aquí no vamos a tocar nada, cualquier cambio del contrato lo haremos en src/graphql/schemas/schema.graphql
Eso si, si hacemos cambios, debemos de ejecutar el comando de nuevo, cosa que es un poco engorroso, por lo que os doy la opción de poder configurar la opción para que esté “escuchando” cambios en el fichero del contrato.
Para habilitar el modo de observación para el script (para generar tipeos automáticamente cada vez que cambie un archivo .graphql
), añadimos la opción de observación al método generate()
.
import { GraphQLDefinitionsFactory } from '@nestjs/graphql';
import { join } from 'path';
const definitionsFactory = new GraphQLDefinitionsFactory();
definitionsFactory.generate({
typePaths: ['./src/**/*.graphql'],
path: join(process.cwd(), 'src/graphql.ts'),
watch: true // <=====
});
Con esto, si ejecutamos el script, nos encontraremos con esta situación:
Por lo tanto, si añado un cambio, cualquiera (luego dejamos como estaba hasta ahora) por ejemplo una nueva definición llamada goodbye
:
type Query {
hello: String!
goodbye: String!
}
...
Y esto es lo que pasará al guardar:
Y en el fichero generado aparecerá la nueva definición, lista para darle solución y usarla:
/*
* -------------------------------------------------------
* THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
* -------------------------------------------------------
*/
...
export interface IQuery {
hello(): string | Promise<string>;
goodbye(): string | Promise<string>;
}
...
(Eliminad esa definición antes de seguir al siguiente punto)
Resolvers
Bien, ya tenemos definido nuestro contrato y generado los elementos desde el schema, pero ¿Qué ocurre si no damos solución a las definiciones (funcionalidades) especificadas?
Es bien sencillo, no podremos usarlas porque no sabrá que nos tiene que dar como respuesta.
Antes de trabajar dándole solución, vamos a ir al playground (donde hacemos las operaciones de consulta de GraphQL) iniciando el proyecto para ver el problema de no haber dado solución a esas definiciones:
npm run start:dev
Abrimos una nueva pestaña en el terminal (para mantener la ejecución a la escucha de cambios en el schema) y accedemos a la URL (http://localhost:3000/graphql).
Con ello accedemos al playground de GraphQL, donde podremos trabajar con las opciones que hemos especificado en nuestro Schema. Si queréis aprender más, ya sabéis que recurso utilizar, es GRATIS.
Mirando el playground, básicamente estas cuatro opciones son las más importantes para hacer lo esencial:
- 1 — Apartado donde escribimos nuestras operaciones de consulta GraphQL, sea
Query
,Mutation
oSubscription
. - 2 — Resultado de la operación
- 3 — Apartado para añadir argumentos dinámicos a nuestras operaciones de GraphQL escritas en 1.
- 4 — Documentación de lo que tenemos disponible, para poder realizar nuestras consultas.
Accedemos al apartado de docs y esto es lo que se ve, como se puede apreciar tenemos un Query
y un Mutation
.
Vamos a realizar la consulta con el Query, cuya definición se llama hello
- 1 — Consulta
- 2 — Ejecución
- 3 — Resultado con error. ¿Cuál es el motivo? Como hemos visto al principio de este punto: no podremos usarlas porque no sabrá que nos tiene que dar como respuesta y esa es nuestra responsabilidad.
Crear un resolver
- En la carpeta
src/graphql
crearemos una nueva carpeta llamadaresolvers
- Dentro de esta carpeta, crea un archivo llamado
graphql.resolver.ts
y pegamos el siguiente código donde ya proporcionamos tanto la solución de laQuery hello
y elMutation add
:
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
@Resolver('GraphQL')
export class GraphQLResolver {
@Query('hello')
async hello() {
return 'Hola, mundo!';
}
@Mutation('add')
async add(@Args('input') input: { a: number, b: number }) {
const { a, b } = input;
return a + b ;
}
}
- Este archivo crea un resolver GraphQL que define dos campos:
hello
yadd
.hello
devuelve una cadena simple, mientras queadd
recibe dos números y devuelve la suma de ellos. - El decorador
Resolver
con el valor GraphQL es indispensable, debemos de ponerlo. - Para definir una
Query
, añadimos su decorador e introducimos el nombre tal como se ha definido. Luego añadimos la función, que podemos ponerle el nombre que queramos, pero como recomendación lo ideal es poner el mismo nombre, para que sea más fácil de asociarlo alQuery
. - Hacemos lo mismo para el
Mutation
, añadir su nombre de la definición y metemos en este caso el elementoArgs
, donde tenemos el argumento input definido en el Schema.
Ahora teniendo esto vamos a realizar el último paso, el de importar el resolver como un provider, para poder consumir las opciones especificadas con las soluciones dadas.
Importar el provider GraphQLResolver
- En el archivo
app.module.ts
importamos el providerGraphQLResolver
:
mport { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
import { GraphQLResolver } from './graphql/resolvers/graphql.resolver';
@Module({
imports: [
...
],
providers: [GraphQLResolver], // <===========
})
export class AppModule { }
- Este archivo importa el módulo
GraphQLModule
y el resolverGraphQLResolver
.
Probando la aplicación
Accedemos de nuevo a http://localhost:3000/graphql
Y añadimos las dos operaciones:
hello
add
Conclusión
En este artículo os he enseñado las nociones básicas para aprender a trabajar con GraphQL en NestJS mediante el enfoque práctico de Schema First
.
Si viese interés (en relación a comentarios en el artículo o RRSS) podría ir haciendo ejemplos más completos pero si no lo hubiese, iría pensando que escribir sobre lo que me llame la atención hablar.
A corto-medio plazo haré un artículo similar a este mediante el enfoque de Code First
para ver sus diferencias.
Os dejo el resultado de lo trabajado:
Presencia en redes sociales
Podéis encontrarme en las siguientes redes.