Nota: Este tutorial, por su complejidad, será divido en dos partes. La primera, que es el presente escrito, será dedicado al backend usando GraphQL y Apollo junto con TypeGraphQL. El segundo, al frontend usando la última versión de Angular.
En el tutorial anterior aprendimos los conceptos básicos de GraphQL. Vimos qué eran las variables, las queries, mutations, entre otras cosas. Porque no es suficiente quedarse con la teoría, vamos a ponernos manos a la obra para practicar lo aprendido.
Preparación del proyecto
Antes que nada, recuerda utilizar la última versión LTS de Node. Así mismo, puedes usar tanto NPM como Yarn.
La lista de paquetes que necesitamos instalar es:
@types/bcryptjs
@types/graphql
@types/lokijs
@types/pino
apollo-server
bcryptjs
class-validator
graphql
lokijs
pino
pino-pretty
reflect-metadata
type-graphql
typedi
Las dependencias de desarrollo son las siguientes:
@types/node
nodemon
ts-node
tslint
typescript
Por último, agrega el script start
para que corra nodemon ejecute ts-node y corra nuestra aplicación:
{
"scripts": {
"start": "nodemon --exec ts-node src/main.ts --watch src/ --ignore src/database/data.json"
}
}
Creando los modelos
Para enfocarnos en GraphQL vamos a usar una base de datos en memoria.
Lo primero será crear los modelos, en nuestro caso solo tenemos uno al cual llamaremos User
:
// user.type.ts
import {
ObjectType,
Field,
Int
} from 'type-graphql'
@ObjectType()
export default class User {
@Field(type => Int)
id: number
@Field()
email: string
@Field()
password: string
}
Este tipo solo contiene tres campos:
- id: representa la PK.
- password
Nota que type-graphql
nos da tipos opcionales como Int
cuando los tipos de JavaScript no nos son suficientes. Por ejemplo, por defecto, number
es mapeado a un Float
de GraphQL. Por esta razón, por medio del parámetro type
, le decimos que es de tipo INT
.
A su vez, esta misma clase será nuestro modelo con el que trabajará el motor de base de datos (siempre pensando en reutilizar 😉).
Creando el servicio
Ahora procedemos a crear el servicio para User
. Este se ve así:
// user.service.ts
import { Service } from 'typedi'
import { hash, genSalt } from 'bcryptjs'
import db from '../database/client'
import User from './user.type'
import UserInput from './user.input'
@Service()
export default class UserService {
private datasource = db.getCollection('users')
findByEmail(email: strng): User {
return this.datasource.findOne({ email })
}
async create(data: UserInput): Promise<User> {
const body = {
...data,
id: this.datsource.count() + 1,
password: await hash(data.password, await genSalt(10))
}
const { id } = this.datasource.insert(body)
return this.find(id)
}
}
Lo primero a notar es que el servicio está anotado con el decorador Service
. Este decorador nos permite registrar una clase como un servicio en el contenedor DI para posteriormente inyectarlo en algún otro sitio.
El resto es realmente simple. Como propiedad tenemos datasource
, la cual contiene la colleción users
que hemos recuperado de la base de datos.
Finalmene tenemos dos métodos los cuales son findByEmail
que encuentra un usuario por medio de su email
y create
el cual recibe un argumento de tipo UserInput
, hashea su password plano, lo inserta en la colección y finalmente retorna el documento creado.
Suena bien, pero, ¿qué viene a ser UserInput
? 🤔
Argumentos personalizados
Recorarás que en el tutorial anterior hablamos de input
, los cuales son tipos que engloban campos para ser pasados como un conjunto a través de un solo argumento en las consultas. Tomando este concepto, procedemos a crear nuestro propio input.
import { IsEmail } from 'class-validator'
import {
InputType,
Field
} from 'type-graphql'
@InputType()
export default class UserInput {
@Field()
@IsEmail()
email: string
@Field()
password: string
}
Te darás cuenta que es muy similar a User
, ¿cierto? La única diferencia es la decoración InputType
, por medio de la cual indicamos que esta clase es una estructura input
. Además, como somos muy cuidadosos, validamos el campo email
por medio de la decoración isMail
, validación propiedad del paquete class-validator
y que será automática, la misma que nos retornará un error a través de GraphQL si proveemos un valor erróneo para el campo.
Creando el Resolver
Bien, hasta aquí ya tenemos los tipos, ahora procedamos a crear la consulta y la mutación con sus respectivos resolvers. Para esto, creamos una clase y la anotamos con Resolver
, como muestro a continuación:
import {
Resolver,
Arg,
Query,
Mutation,
Int
} from 'type-graphql'
import User from './user.type'
@Resolver(of => User)
export default class UserResolver {
}
Por medio de la decoración Resolver
indicamos que esta clase contendrá uno o más resolvers y además, por medio del argumento of
le indicamos a quién pertenecerá; en este caso, a User
. Ahora procedemos a incluir el servicio de User
para consultar a la base de datos y retornar desde las consultas y mutaciones.
// imports anteriores
import { Inject } from 'typedi'
@Resolver(of => User)
export default class UserResolver {
@Inject() private service: UserService
}
Listo. Pero, ¿Qué ha pasado aquí? 🤔
La decoración @Inject
"inyecta" la dependencia (una instancia) en una variable o argumento, dependencia que debe ser del mismo tipo que el de la variable. Cuando hacemos uso de @Inject
lo que hacemos es decirle al contenedor:
Hey, ¿Puedes buscar en tu registro si tienes una clase llamada ´
UserService
? Si la tienes, necesito que me des una instancia y la guardes en la variableservice
.
¿Se entendió? Genial. Una vez que ya hemos incluido la dependencia de UserService
ya estamos listos para usar sus métodos. Ahora, definamos nuestra Query
. Esta se encargará de encontrar un usuario por medio de su id
:
// imports anteriores
import {
...
Arg, // agregamos
Query, // agregamos
Int // agregamos
} from 'type-graphql'
@Resolver(of => User)
export default class UserResolver {
...
@Query(returns => User, { nullable: true })
user(@Arg('email') email: string) {
return this.userService.findByEmail(email)
}
}
Por medio del decorador Query
indicamos que dicho método representa una consulta. Esta decoración acepta dos parámetros: el tipo de retorno y un array de opciones, el cual es opcional. Por medio de ese array le decimos que esta consulta puede retornar null, debido a que cuando un usuario no se encuentre, lo que será retornado será null
. En caso contrario obtendríamos un error al retornar null
.
En el argumento id
, proveemos un decorador de tipo Arg
, al cual le pasamos un nombre. Finalmente, cuando se ejecute el método, buscará en la base de datos ese email
y retornará el usuario asociado.
La anterior definición se traduce al siguiente esquema GraphQL:
type Query {
user(email: String!): User
}
Sencillo, ¿no?. Ahora seguimos con nuestra mutación, la cual será encargada de crear un usuario en la base de datos. La definición del método es bastante similar a la consulta:
// imports anteriores
import {
...
Mutation // agregamos
} from 'type-graphql'
import UserInput from './user.input'
@Resolver(of => User)
export default class UserResolver {
...
@Mutation(returns => User)
user(@Arg('data') data: UserInput) {
return this.userService.create(data)
}
}
Fíjate en el argumento del método, ya no le pasamos el type
en el decorador Arg
porque ya lo hacemos por medio de Typescript. Lo que hará type-graphql es usar Reflection para ver los tipos de los parámetros y hacer el mapeo correcto. ¡Es genial!
Lo anterior se traducirá a lo siguiente:
type Mutation {
createUser(data: UserInput!): User
}
DI, Base de datos y Apollo
Ya tenemos casi todo lo que necesitamos, solo nos falta unos pequeños pasos. El primero es configurar nuestro container de inyección de dependencias. Para esto, hacemos lo siguiente:
import { Container } from 'typedi'
import { useContainer } from 'type-graphql'
export default () => {
useContainer(Container)
}
Importamos el container desde typedi
y se lo pasamos a type-graphql
para que lo configure por nosotros. Eso es todo lo que necesitamos hacer para tenerlo funcionando y poder proveer e inyectar dependencias.
Lo siguiente es crear nuestra bases de datos. Como dijimos al inicio del tutorial, será una base de datos en memoria, así que como es de suponer, el setup será sencillísimo:
// database/bootstrap.ts
import * as Loki from 'lokijs'
const db: Loki = new Loki('data.json')
db.addCollection('users')
export default db
Fíjate que en el momento en que instanciamos la base de datos, creamos una colección llamada users
, que es donde se guardarán los usuarios que vayamos creando.
Finalmente, necesitamos crear nuestro servidor GraphQL usando Apollo. Veamos como luce:
// server/index.ts
import { ApolloServer } from 'apollo-server'
import { buildSchema } from 'type-graphql'
import formatError from '../errors/argument.format'
import UserResolver from '../users/user.resolver'
/**
* Creates a Apollo server using an
* executable schema generated by
* TypeGraphQL.
* We provide a custom Apollo error
* format to returns a non-bloated
* response to the client.
*/
export default async () => {
const schema = await buildSchema({
resolvers: [
UserResolver
]
})
return new ApolloServer({
schema
})
}
Lo primero que hacemos es importar los resolvers, luego pasárselos a buildSchema
en forma de array para que nos genera un schema
de GraphQL válido que pueda entender Apollo. Lo segundo, es instanciar ApolloServer
y pasarle el schema
junto con otras propiedades opcionales. Puedes ver la lista de propiedades aquí. Una vez hecho esto, ya tenemos un servidor listo para correr.
Entry point
Para terminar, creamos el archivo principal que pondrá a correr el servidor de Apollo. Para esto, solo importamos la función que crea el servidor y ejecutamos la función listen
, la cual pondrá en escucha al servidor.
// main.ts
import 'reflect-metadata'
import enableDI from './container/bootstrap'
import createServer from './server'
import log from './logger'
const run = async () => {
enableDI()
try {
const server = await createServer()
const { url } = await server.listen({ port: 3000 })
log.info(`🚀 Server ready at ${url}`)
} catch (e) {
log.error(e)
}
}
run()
Opcional
Error Formatter
Por defecto, cuando ocurre un error en tiempo de ejecución, GraphQL nos devuelve un gran objeto con muchos detalles, como en qué línea ocurrió, el stack trace, entre otras. Para no exponer demasiados detalles por seguridad y por simpleza, podemos crear un formateador que intercepte el error y lo modifique a nuestro antojo. Veamos un ejemplo:
// errors/argument.format.ts
import { GraphQLError } from 'graphql'
import { ArgumentValidationError } from 'type-graphql'
import { ValidationError } from 'class-validator'
/**
* Describes a custom GraphQL error format.
* @param { err } Original GraphQL Error
* @returns formatted error
*/
export default (err: GraphQLError): any => {
const formattedError: { [key: string]: any } = {
message: err.message
}
if (err.originalError instanceof ArgumentValidationError) {
formattedError.validationErrors = err.originalError.validationErrors.map((ve: ValidationError) => {
const constraints = { ...ve.constraints }
return {
property: ve.property,
value: ve.value,
constraints
}
})
}
return formattedError
}
Los formateadores de errores reciben un error de tipo GraphQL
. Este error contiene propiedades como message
, paths
, location
, extensions
, entre otras. Sin embargo, podemos solo extrar lo que necesitemos. En este caso, solo necesitamos el mensaje y los errores de validación sin mucho detalle: solo la propiedad donde ocurrió el error, su valor y las restricciones que no pasó. De esta manera tenemos errores personalizados.
Para habilitarlo, solo se lo pasamos a la opción formatError
del constructor de ApolloServer
:
return new ApolloServer({
schema,
formatError
})
}
Run, Forrest, Run!
Llegó la hora de la verdad. En este punto no hay marcha atrás: o corre o te disparas en la sien 😝 Para correr el servidor ejecuta el clásico npm start
.
Si vamos a localhost:3000 veremos el Playground para empezar a jugar. ¡Ejecuta la consulta y mutación que aparece en la imagen para ver los resultados!
En la próxima entrega de esta serie veremos como consumir esta API desde Angular usando el cliente de Apollo. ¡Nos vemos! 🤘
Top comments (3)
Esta genial el contenido, buen trabajo.
Actualmente tengo un error al intentar hacer una consulta.
"message": "Variable \"$id\" of required type \"ID!\" was not provided.",
pero no entiendo el porque la razon?
Qué interesante!
Es el primer artículo técnico que leo en español. Me gusta. No sabía que hay DI con typescript. No uso TypeScript pero en C# usamos eso.
Gracias por compartir
Wow, ¡gracias por tu feedback, Peter! Como siempre, es un placer contribuir :)