GraphQL es una forma potente y eficiente de crear APIs. El cliente consulta la API, de forma parecida a como lo haría con una base de datos, y esa API devuelve solo los datos solicitados, lo que suele reducir el tamaño de la respuesta y mejorar los tiempos de carga. En el mundo actual, los tiempos de respuesta bajos son clave.
Cuando combinas una API GraphQL con MongoDB, no solo obtienes respuestas rápidas: también trabajas con un formato de datos coherente de principio a fin.
Imagina esto: tu cliente ejecuta una consulta GraphQL con una estructura similar a JSON. Cuando los datos llegan a tu aplicación —pongamos TypeScript en este ejemplo— sigues trabajando con un formato parecido a JSON. Y si damos un paso más, al usar MongoDB, los datos que envías y recibes también se parecen a JSON. En resumen, obtienes una experiencia de datos coherente con gran rendimiento. No tendrás que preocuparte tanto de manipular y dar formato a los datos; podrás centrarte en la experiencia de usuario de tu aplicación y no en la base de datos ni en las herramientas.
En este tutorial verás lo sencillo que es usar MongoDB en tu API GraphQL, esta vez con TypeScript.
Requisitos previos
Para este tutorial, asegúrate de tener listo lo siguiente antes de empezar:
Un clúster de MongoDB Atlas
Node.js 22+
MongoDB Node.js Driver 6.x
En este tutorial no haremos nada especialmente complejo a nivel de base de datos, así que sirve cualquier nivel de clúster de MongoDB, incluso el gratuito. No obstante, aquí no veremos cómo aprovisionar un clúster de MongoDB Atlas. Se da por hecho que ya tienes configuradas las reglas de usuario y de red. Si necesitas ayuda para empezar, consulta la documentación de introducción a Atlas.
Necesitarás crear una aplicación Node.js. Si quieres, puedes ejecutar los siguientes comandos:
Estos comandos crean un directorio de proyecto y un package.json básico. Usaremos TypeScript, Express Framework, MongoDB y Apollo en este proyecto. Instálalos con estos comandos:
npminstall @apollo/server@4 body-parser cors dotenv express@4 graphql mongodb
npminstall @types/cors @types/express@4 @types/node tsx typescript --save-devCon la tecnología de
Estos comandos instalarán las dependencias y también las definiciones de tipos para usarlas en TypeScript.
Como último requisito, necesitas un archivo de configuración para TypeScript. En la raíz del proyecto, crea un tsconfig.json con el siguiente JSON:
{"compilerOptions":{"target":"ES2020","module":"commonjs","lib":["ES2020"],"outDir":"./dist","rootDir":"./src","strict":true,"esModuleInterop":true,"skipLibCheck":true,"forceConsistentCasingInFileNames":true,"resolveJsonModule":true,"moduleResolution":"node","declaration":true,"declarationMap":true,"sourceMap":true},"include":["src/**/*"],"exclude":["node_modules","dist"]}Con la tecnología de
Crea la base con Apollo, Express y las dependencias de MongoDB
A nivel de estructura del proyecto, usaremos lo siguiente:
src/database/connection.ts
src/database/userService.ts
src/resolvers/index.ts
src/schema/typeDefs.ts
src/types/index.ts
src/index.ts
.env
El archivo src/index.ts contendrá todo el arranque de Express Framework y Apollo. Es decir, el inicio del servidor, la definición del endpoint GraphQL, etc. En src/types/index.ts estarán las definiciones TypeScript para trabajar con el modelo de datos de la base de datos dentro de la aplicación. No lo confundas con src/schema/typeDefs.ts que, aunque similar, se centra en las definiciones de tipos de GraphQL.
Esto nos deja con los directorios database y resolvers. La mayor parte de la lógica de aplicación y base de datos estará ahí; lo veremos enseguida.
Por ahora, centrémonos en configurar el servidor en src/index.ts:
import'dotenv/config';import{ ApolloServer }from'@apollo/server';import{ expressMiddleware }from'@apollo/server/express4';import express from'express';import http from'http';import cors from'cors';import bodyParser from'body-parser';import{ typeDefs }from'./schema/typeDefs';import{ resolvers }from'./resolvers';asyncfunctionstartServer(){try{const app =express();const httpServer = http.createServer(app);const server =newApolloServer({
typeDefs,
resolvers,});await server.start();
app.use('/graphql',cors<cors.CorsRequest>(),
bodyParser.json(),expressMiddleware(server));constPORT= process.env.PORT||3000;awaitnewPromise<void>((resolve)=> httpServer.listen({ port:PORT}, resolve));console.log(Server ready at http://localhost:${PORT}/graphql);}catch(error){console.error('Error starting server:', error);
process.exit(1);}}startServer();Con la tecnología de
Iremos ampliando este archivo según avancemos.
Para empezar, importamos todas las dependencias del proyecto. Después, configuramos el servidor de Express y el middleware de Apollo.
Aún no hemos añadido nada a src/schema/typeDefs.ts ni a src/resolvers/index.ts, pero en esta fase lo estamos dejando todo listo. No podrás ejecutar el servidor sin errores hasta que completemos lo que falta en el resto de archivos.
Conviene mencionar que este ejemplo permite peticiones desde todos los orígenes por lo siguiente:
Esto está bien en desarrollo, pero en producción limita los orígenes solo a los que necesites.
Interactuar con MongoDB desde la aplicación TypeScript con Node.js
Aunque ya tenemos la base de Express y Apollo, aún no hemos configurado MongoDB en la aplicación. Antes de meternos en el código, añade la información de conexión al archivo .env:
MONGODB_URI="mongodb+srv://<USERNAME>:<PASSWORD>@<HOST>/?appName=devrel-graphql"
DB_NAME="graphql_example"Con la tecnología de
Sustituye <USERNAME>, <PASSWORD> y <HOST> con los datos de tu clúster de MongoDB Atlas, que encontrarás en tu panel de MongoDB Atlas.
Ahora, vamos a establecer la lógica de conexión a la base de datos. En el archivo **src/database/connection.ts**, añade lo siguiente:
import{ MongoClient, Db }from'mongodb';let db: Db |null=null;let client: MongoClient |null=null;exportasyncfunctionconnectToDatabase():Promise<Db>{if(db){return db;}try{constMONGODB_URI= process.env.MONGODB_URI;if(!MONGODB_URI){thrownewError('MONGODB_URI is not set');}constDB_NAME= process.env.DB_NAME||'graphql_example';constAPP_NAME= process.env.APP_NAME||'devrel-graphql';console.log('Connecting to MongoDB...');
client =newMongoClient(MONGODB_URI,{ appName:APP_NAME});await client.connect();
db = client.db(DB_NAME);console.log(Successfully connected to MongoDB database: ${DB_NAME});return db;}catch(error){console.error('MongoDB connection error:', error);throw error;}}exportfunctiongetDatabase(): Db {if(!db){thrownewError('Database not initialized. Call connectToDatabase() first.');}return db;}exportasyncfunctioncloseDatabase():Promise<void>{if(client){await client.close();
db =null;
client =null;console.log('MongoDB connection closed');}}Con la tecnología de
La idea aquí es crear una conexión singleton. Hay muchas otras formas de integrar MongoDB en tu aplicación, pero esta es la ruta que elegimos para este ejemplo.
La función connectToDatabase toma la información de conexión del archivo **.env**, se conecta y devuelve la base de datos definida. Si falla, captura y relanza el error. Solo deberías usar connectToDatabase al arrancar el servidor, como viste en el archivo anterior.
La función getDatabase se usará cada vez que llegue una petición a nuestra aplicación. Así obtenemos la instancia actual de la conexión.
Y por último, para dejarlo todo limpio, tenemos closeDatabase para cerrar la conexión cuando el servidor se detiene. Depende de ti si quieres incluirla, pero es una buena práctica.
Antes de entrar en la lógica de base de datos, es buena idea definir los tipos de TypeScript para nuestros DTO (data transfer objects):
exportinterfaceUser{
id:string;
name:string;
email:string;
age?:number;
createdAt:string;}exportinterfaceCreateUserInput{
name:string;
email:string;
age?:number;}exportinterfaceUpdateUserInput{
id:string;
name?:string;
email?:string;
age?:number;}Con la tecnología de
Estas interfaces, en **src/types/index.ts**, las gestiona nuestra API, pero la lógica de base de datos también debe conocerlas. Lo verás enseguida.
Pasamos ya a las operaciones reales con la base de datos: crear, leer, actualizar y borrar (CRUD). Esto lo haremos en src/database/userService.ts. La idea es que los resolvers de GraphQL gestionen la lógica de aplicación y llamen a las funciones de este servicio, y el servicio gestione la lógica de base de datos.
En src/database/userService.ts tenemos el siguiente código:
interfaceUserDocument{
_id: ObjectId;
name:string;
email:string;
age?:number;
createdAt: Date;}Con la tecnología de
Se parece a nuestra interfaz User, pero no es exactamente igual. UserDocument refleja con más fidelidad cómo trabaja la base de datos. En la base usamos ObjectId, mientras que en la aplicación usamos cadenas. En MongoDB, el id se representa como _id en lugar de id, como en la interfaz User.
Con esto, pasamos a la clase UserService.
Tras obtener el handler de la colección desde la instancia de base de datos en el constructor, tenemos esta serie de funciones:
toUser
getAllUsers
getUserById
createUser
updateUser
deleteUser
createIndexes
La función toUser es básicamente un helper que convierte entre User y UserDocument. En la base de datos enviamos y recibimos UserDocument, y necesitamos convertirlo para manejar User en la aplicación.
asynccreateIndexes():Promise<void>{awaitthis.collection.createIndex({ email:1},{ unique:true});awaitthis.collection.createIndex({ createdAt:-1});console.log('Database indexes created');}Con la tecnología de
La función createIndexes creará algunos índices de ejemplo que pueden ser útiles. Por defecto, solo existe un índice en el campo _id, lo cual vale para una demo con pocos documentos, pero a medida que tu aplicación crezca, no ignores la creación adecuada de índices según tus datos y consultas. Los dos de arriba son solo ejemplos.
Ahora podemos revisar cada función del servicio empezando por getAllUsers:
asyncgetAllUsers():Promise<User[]>{const users =awaitthis.collection.find().toArray();return users.map(doc =>this.toUser(doc));}Con la tecnología de
Esta función devuelve todos los documentos de la colección porque no hay filtro en find. Convertimos los resultados con toUser y los devolvemos a la aplicación, que más adelante serán nuestros resolvers de Apollo.
asyncgetUserById(id:string):Promise<User |null>{try{const doc =awaitthis.collection.findOne({ _id:newObjectId(id)});return doc ?this.toUser(doc):null;}catch(error){returnnull;}}Con la tecnología de
La función getByUserId es similar, pero aquí usamos findOne con un criterio de filtro: queremos un único usuario por su campo único _id. Como en la interfaz User el id es una cadena, lo envolvemos con ObjectId para que sea válido en las consultas a MongoDB.
Pasamos a crear datos en MongoDB con createUser:
asynccreateUser(input: CreateUserInput):Promise<User>{const newUser: Omit<UserDocument,'_id'>={
name: input.name,
email: input.email,
age: input.age,
createdAt:newDate(),};const result =awaitthis.collection.insertOne(newUser as UserDocument);const createdUser =awaitthis.collection.findOne({ _id: result.insertedId });if(!createdUser){thrownewError('Failed to create user');}returnthis.toUser(createdUser);}Con la tecnología de
Tras insertar con insertOne, usamos el insertedId devuelto para consultar ese mismo documento. Lo hacemos porque en una API GraphQL, cuando se crea un dato, normalmente se espera poder consultarlo en la misma mutación.
La función updateUser es un poco distinta:
asyncupdateUser(input: UpdateUserInput):Promise<User |null>{try{const updateFields: Partial<Omit<UserDocument,'_id'>>={};if(input.name !==undefined) updateFields.name = input.name;if(input.email !==undefined) updateFields.email = input.email;if(input.age !==undefined) updateFields.age = input.age;const result =awaitthis.collection.findOneAndUpdate({ _id:newObjectId(input.id)},{ $set: updateFields },{ returnDocument:'after'});return result ?this.toUser(result):null;}catch(error){returnnull;}}Con la tecnología de
En esta aplicación solo permitimos actualizar los campos name, email y age. Todos son opcionales, así que los añadimos al criterio de actualización solo si están presentes.
Esto nos lleva a findOneAndUpdate.
El primer objeto es el filtro: solo queremos actualizar donde _id coincide. Para cualquier coincidencia usamos el operador $set para reemplazar o crear los campos de updateFields. Los campos que no aparezcan no se tocan. Por último, como buscamos y actualizamos, establecemos returnDocument en after para recibir el documento ya modificado y no el original. Si usáramos updateOne en lugar de findOneAndUpdate, solo obtendríamos información de la operación, no los datos.
La última operación CRUD es borrar datos de MongoDB con deleteUser:
asyncdeleteUser(id:string):Promise<boolean>{try{const result =awaitthis.collection.deleteOne({ _id:newObjectId(id)});return result.deletedCount >0;}catch(error){returnfalse;}}Con la tecnología de
La función deleteOne se parece a findOne porque usamos un filtro por _id. La diferencia es que cualquier coincidencia —como máximo un documento— se borrará.
A estas alturas, la lógica de base de datos ya está lista. Para finalizarla, conecta desde **src/index.ts**:
// Previous imports here...import{ connectToDatabase, closeDatabase }from'./database/connection';import{ UserService }from'./database/userService';asyncfunctionstartServer(){try{awaitconnectToDatabase();const userService =newUserService();await userService.createIndexes();// The rest of the file here...}Con la tecnología de
En este fragmento hemos omitido la mayor parte de src/index.ts. El objetivo es mostrar el uso de connectToDatabase y la creación de índices. En resumen, añade esas tres líneas al principio de tu función startServer.
A partir de aquí, podemos centrarnos en la lógica de la API.
Define los tipos de GraphQL, los resolvers y la lógica de aplicación
Ya tenemos definidos los tipos de TypeScript para trabajar con la base y para mover datos por la aplicación. Pero aún no hemos creado las definiciones de tipos de GraphQL, que determinan con qué puede interactuar el usuario al usar nuestra API.
En src/schema/typeDefs.ts, añade lo siguiente:
exportconst typeDefs = #graphql
typeUser{
id:ID!
name: String!
email: String!
age: Int
createdAt: String!}typeQuery{
users:[User!]!user(id:ID!): User
}typeMutation{createUser(name: String!, email: String!, age: Int): User!updateUser(id:ID!, name: String, email: String, age: Int): User
deleteUser(id:ID!): Boolean!};Con la tecnología de
El tipo User define qué puede solicitar el cliente en una consulta. En este ejemplo coincide con la interfaz User, pero no tiene por qué. Por ejemplo, quizá tu interfaz User incluye datos que nunca querrás devolver al cliente, como una contraseña.
El tipo Query define qué consultas se pueden ejecutar en la API. Cada consulta devuelve un User o un array de User, pero podemos añadir lógica específica en medio.
El tipo Mutation es similar a Query, pero pensado para operaciones que modifican datos.
Con las definiciones de tipos listas, podemos centrarnos en la lógica de los resolvers. En src/resolvers/index.ts, incluye lo siguiente:
import{ UserService }from'../database/userService';import{ User, CreateUserInput, UpdateUserInput }from'../types';exportconst resolvers ={
Query:{
users:async():Promise<User[]>=>{const userService =newUserService();returnawait userService.getAllUsers();},
user:async(_:unknown,{ id }:{ id:string}):Promise<User |null>=>{const userService =newUserService();returnawait userService.getUserById(id);},},
Mutation:{
createUser:async(
_:unknown,{ name, email, age }: CreateUserInput
):Promise<User>=>{const userService =newUserService();returnawait userService.createUser({ name, email, age });},
updateUser:async(
_:unknown,{ id, name, email, age }: UpdateUserInput
):Promise<User |null>=>{const userService =newUserService();returnawait userService.updateUser({ id, name, email, age });},
deleteUser:async(_:unknown,{ id }:{ id:string}):Promise<boolean>=>{const userService =newUserService();returnawait userService.deleteUser(id);},},};Con la tecnología de
Estos resolvers son la extensión natural de lo que vimos en el archivo de definiciones de tipos para consultas y mutaciones.
Fíjate en la consulta users:
users:async():Promise<User[]>=>{const userService =newUserService();returnawait userService.getAllUsers();},Con la tecnología de
Esta consulta GraphQL usa nuestro UserService, parte de la lógica de base de datos, para obtener todos los usuarios. El User[] resultante se devuelve al cliente, pero solo con los campos que haya pedido y que estén definidos en los tipos GraphQL.
En este ejemplo tenemos más campos disponibles, pero el cliente solo quiere name y email. Esto se gestiona en base a nuestras definiciones de tipos GraphQL. Apollo se encarga de la magia.
Estas mismas reglas aplican a cada consulta y mutación en **src/resolvers/index.ts**.
createUser:async(
_:unknown,{ name, email, age }: CreateUserInput
):Promise<User>=>{const userService =newUserService();returnawait userService.createUser({ name, email, age });},Con la tecnología de
En la definición de createUser vimos que el cliente puede pasar name, email y age. En el resolver hacemos lo mismo y llamamos a createUser en nuestro servicio de base de datos. La respuesta se devuelve al cliente con los campos que haya solicitado.
Compila y ejecuta la API GraphQL
Llegados a este punto, la API GraphQL debería estar en muy buen estado. Hay varias formas de compilar y ejecutar la aplicación.
Por ejemplo, puedes añadir lo siguiente a tu **package.json**:
"scripts":{"build":"tsc","start":"node dist/index.js","dev":"tsx src/index.ts","dev:watch":"tsx watch src/index.ts","watch":"tsc -w"},Con la tecnología de
Si optas por esta vía, ejecuta npm run build para compilar el proyecto y npm run start para servirlo. El puerto por defecto es el 3000, y puedes probarlo con cURL o la herramienta que prefieras.
Conclusión
Has visto cómo crear una API GraphQL con TypeScript y MongoDB como base de datos. Aunque el ejemplo es sencillo, hemos construido consultas y mutaciones para realizar operaciones CRUD contra la base. MongoDB encaja a la perfección en una API GraphQL porque el formato de los datos es similar de principio a fin. Por ejemplo, el cliente envía una consulta con apariencia de JSON. Los datos que viajan por la consulta pueden usarse con un formateo mínimo, y lo mismo al enviarlos a la base y recibirlos de ella. La experiencia es fluida y te permite centrarte en la experiencia de usuario y no en manipular datos para ajustarlos a la base.
GraphQL mola porque puedes consultar una API como si fuera una base de datos y pedir solo lo que necesitas. Eso hace que el cliente sea más ligero y eficiente.
¿Es Apollo la única opción para GraphQL?⌄
Apollo facilita mucho crear APIs GraphQL, aunque no es la única opción. Sin embargo, el resto de alternativas se salen del alcance de este tutorial.
¿Por qué usar MongoDB con GraphQL?⌄
Ambos trabajan con formatos de datos similares a JSON, así que no tienes que estar convirtiendo estructuras a cada paso desde la base de datos hasta la API y el cliente.
¿Qué hace un resolver?⌄
Un resolver es una función que le indica a GraphQL cómo obtener o modificar los datos de un campo concreto de tu esquema.
¿Cuál es la diferencia entre una query y una mutación?⌄
Las queries sirven para leer/recuperar datos (como las peticiones GET), mientras que las mutaciones son para crear, actualizar o borrar datos (como POST, PUT, DELETE).
Autor
Nic Raboy
Nic Raboy es Jefe de Relaciones con Desarrolladores en MongoDB, donde dirige un equipo de programadores de Python, Java, C# y PHP que crean contenido impresionante para ayudar a los programadores a incluir con éxito MongoDB en sus proyectos. Tiene experiencia con Golang y JavaScript y escribe a menudo sobre muchas de sus aventuras de desarrollo.
Aprende algunas posibilidades de modelado de datos que incluyen documentos anidados al diseñar un sistema de gestión de contenidos (CMS) o una aplicación de blog.
Aprende a crear, conectar, gestionar, consultar y proteger tu base de datos Azure SQL. Esta guía paso a paso cubre todo lo esencial para una configuración óptima de la base de datos.
Esta guía completa te enseña los fundamentos del backend con Python. Aprende conceptos básicos, frameworks y buenas prácticas para empezar a crear aplicaciones web.
Aprende a trabajar con JSON en Python, incluyendo serialización, deserialización, formateo, optimización del rendimiento, manejo de API y comprensión de las limitaciones y alternativas de JSON.
GraphQL es una forma potente y eficiente de crear APIs. El cliente consulta la API, de forma parecida a como lo haría con una base de datos, y esa API devuelve solo los datos solicitados, lo que suele reducir el tamaño de la respuesta y mejorar los tiempos de carga. En el mundo actual, los tiempos de respuesta bajos son clave.
Cuando combinas una API GraphQL con MongoDB, no solo obtienes respuestas rápidas: también trabajas con un formato de datos coherente de principio a fin.
Imagina esto: tu cliente ejecuta una consulta GraphQL con una estructura similar a JSON. Cuando los datos llegan a tu aplicación —pongamos TypeScript en este ejemplo— sigues trabajando con un formato parecido a JSON. Y si damos un paso más, al usar MongoDB, los datos que envías y recibes también se parecen a JSON. En resumen, obtienes una experiencia de datos coherente con gran rendimiento. No tendrás que preocuparte tanto de manipular y dar formato a los datos; podrás centrarte en la experiencia de usuario de tu aplicación y no en la base de datos ni en las herramientas.
En este tutorial verás lo sencillo que es usar MongoDB en tu API GraphQL, esta vez con TypeScript.
Requisitos previos
Para este tutorial, asegúrate de tener listo lo siguiente antes de empezar:
En este tutorial no haremos nada especialmente complejo a nivel de base de datos, así que sirve cualquier nivel de clúster de MongoDB, incluso el gratuito. No obstante, aquí no veremos cómo aprovisionar un clúster de MongoDB Atlas. Se da por hecho que ya tienes configuradas las reglas de usuario y de red. Si necesitas ayuda para empezar, consulta la documentación de introducción a Atlas.
Necesitarás crear una aplicación Node.js. Si quieres, puedes ejecutar los siguientes comandos:
Estos comandos crean un directorio de proyecto y un package.json básico. Usaremos TypeScript, Express Framework, MongoDB y Apollo en este proyecto. Instálalos con estos comandos:
Estos comandos instalarán las dependencias y también las definiciones de tipos para usarlas en TypeScript.
Como último requisito, necesitas un archivo de configuración para TypeScript. En la raíz del proyecto, crea un tsconfig.json con el siguiente JSON:
Si en algún momento quieres ver el proyecto completo o probarlo, puedes echarle un vistazo en GitHub.
Crea la base con Apollo, Express y las dependencias de MongoDB
A nivel de estructura del proyecto, usaremos lo siguiente:
El archivo src/index.ts contendrá todo el arranque de Express Framework y Apollo. Es decir, el inicio del servidor, la definición del endpoint GraphQL, etc. En src/types/index.ts estarán las definiciones TypeScript para trabajar con el modelo de datos de la base de datos dentro de la aplicación. No lo confundas con src/schema/typeDefs.ts que, aunque similar, se centra en las definiciones de tipos de GraphQL.
Esto nos deja con los directorios database y resolvers. La mayor parte de la lógica de aplicación y base de datos estará ahí; lo veremos enseguida.
Por ahora, centrémonos en configurar el servidor en src/index.ts:
Iremos ampliando este archivo según avancemos.
Para empezar, importamos todas las dependencias del proyecto. Después, configuramos el servidor de Express y el middleware de Apollo.
Aún no hemos añadido nada a src/schema/typeDefs.ts ni a src/resolvers/index.ts, pero en esta fase lo estamos dejando todo listo. No podrás ejecutar el servidor sin errores hasta que completemos lo que falta en el resto de archivos.
Conviene mencionar que este ejemplo permite peticiones desde todos los orígenes por lo siguiente:
Esto está bien en desarrollo, pero en producción limita los orígenes solo a los que necesites.
Interactuar con MongoDB desde la aplicación TypeScript con Node.js
Aunque ya tenemos la base de Express y Apollo, aún no hemos configurado MongoDB en la aplicación. Antes de meternos en el código, añade la información de conexión al archivo .env:
Sustituye
<USERNAME>,<PASSWORD>y<HOST>con los datos de tu clúster de MongoDB Atlas, que encontrarás en tu panel de MongoDB Atlas.Ahora, vamos a establecer la lógica de conexión a la base de datos. En el archivo **src/database/connection.ts**, añade lo siguiente:
La idea aquí es crear una conexión singleton. Hay muchas otras formas de integrar MongoDB en tu aplicación, pero esta es la ruta que elegimos para este ejemplo.
La función
connectToDatabasetoma la información de conexión del archivo **.env**, se conecta y devuelve la base de datos definida. Si falla, captura y relanza el error. Solo deberías usarconnectToDatabaseal arrancar el servidor, como viste en el archivo anterior.La función
getDatabasese usará cada vez que llegue una petición a nuestra aplicación. Así obtenemos la instancia actual de la conexión.Y por último, para dejarlo todo limpio, tenemos
closeDatabasepara cerrar la conexión cuando el servidor se detiene. Depende de ti si quieres incluirla, pero es una buena práctica.Antes de entrar en la lógica de base de datos, es buena idea definir los tipos de TypeScript para nuestros DTO (data transfer objects):
Estas interfaces, en **src/types/index.ts**, las gestiona nuestra API, pero la lógica de base de datos también debe conocerlas. Lo verás enseguida.
Pasamos ya a las operaciones reales con la base de datos: crear, leer, actualizar y borrar (CRUD). Esto lo haremos en src/database/userService.ts. La idea es que los resolvers de GraphQL gestionen la lógica de aplicación y llamen a las funciones de este servicio, y el servicio gestione la lógica de base de datos.
En src/database/userService.ts tenemos el siguiente código:
Lo primero que verás arriba es esta interfaz:
Se parece a nuestra interfaz
User, pero no es exactamente igual.UserDocumentrefleja con más fidelidad cómo trabaja la base de datos. En la base usamosObjectId, mientras que en la aplicación usamos cadenas. En MongoDB, el id se representa como_iden lugar deid, como en la interfazUser.Con esto, pasamos a la clase
UserService.Tras obtener el handler de la colección desde la instancia de base de datos en el
constructor, tenemos esta serie de funciones:La función
toUseres básicamente un helper que convierte entreUseryUserDocument. En la base de datos enviamos y recibimosUserDocument, y necesitamos convertirlo para manejarUseren la aplicación.La función
createIndexescreará algunos índices de ejemplo que pueden ser útiles. Por defecto, solo existe un índice en el campo_id, lo cual vale para una demo con pocos documentos, pero a medida que tu aplicación crezca, no ignores la creación adecuada de índices según tus datos y consultas. Los dos de arriba son solo ejemplos.Ahora podemos revisar cada función del servicio empezando por
getAllUsers:Esta función devuelve todos los documentos de la colección porque no hay filtro en
find. Convertimos los resultados contoUsery los devolvemos a la aplicación, que más adelante serán nuestros resolvers de Apollo.La función
getByUserIdes similar, pero aquí usamosfindOnecon un criterio de filtro: queremos un único usuario por su campo único_id. Como en la interfazUserelides una cadena, lo envolvemos conObjectIdpara que sea válido en las consultas a MongoDB.Pasamos a crear datos en MongoDB con
createUser:Tras insertar con
insertOne, usamos elinsertedIddevuelto para consultar ese mismo documento. Lo hacemos porque en una API GraphQL, cuando se crea un dato, normalmente se espera poder consultarlo en la misma mutación.La función
updateUseres un poco distinta:En esta aplicación solo permitimos actualizar los campos
name,emailyage. Todos son opcionales, así que los añadimos al criterio de actualización solo si están presentes.Esto nos lleva a
findOneAndUpdate.El primer objeto es el filtro: solo queremos actualizar donde
_idcoincide. Para cualquier coincidencia usamos el operador$setpara reemplazar o crear los campos deupdateFields. Los campos que no aparezcan no se tocan. Por último, como buscamos y actualizamos, establecemosreturnDocumentenafterpara recibir el documento ya modificado y no el original. Si usáramosupdateOneen lugar defindOneAndUpdate, solo obtendríamos información de la operación, no los datos.La última operación CRUD es borrar datos de MongoDB con
deleteUser:La función
deleteOnese parece afindOneporque usamos un filtro por_id. La diferencia es que cualquier coincidencia —como máximo un documento— se borrará.A estas alturas, la lógica de base de datos ya está lista. Para finalizarla, conecta desde **src/index.ts**:
En este fragmento hemos omitido la mayor parte de src/index.ts. El objetivo es mostrar el uso de
connectToDatabasey la creación de índices. En resumen, añade esas tres líneas al principio de tu funciónstartServer.A partir de aquí, podemos centrarnos en la lógica de la API.
Define los tipos de GraphQL, los resolvers y la lógica de aplicación
Ya tenemos definidos los tipos de TypeScript para trabajar con la base y para mover datos por la aplicación. Pero aún no hemos creado las definiciones de tipos de GraphQL, que determinan con qué puede interactuar el usuario al usar nuestra API.
En src/schema/typeDefs.ts, añade lo siguiente:
El tipo
Userdefine qué puede solicitar el cliente en una consulta. En este ejemplo coincide con la interfazUser, pero no tiene por qué. Por ejemplo, quizá tu interfazUserincluye datos que nunca querrás devolver al cliente, como una contraseña.El tipo
Querydefine qué consultas se pueden ejecutar en la API. Cada consulta devuelve unUsero un array deUser, pero podemos añadir lógica específica en medio.El tipo
Mutationes similar aQuery, pero pensado para operaciones que modifican datos.Con las definiciones de tipos listas, podemos centrarnos en la lógica de los resolvers. En src/resolvers/index.ts, incluye lo siguiente:
Estos resolvers son la extensión natural de lo que vimos en el archivo de definiciones de tipos para consultas y mutaciones.
Fíjate en la consulta
users:Esta consulta GraphQL usa nuestro
UserService, parte de la lógica de base de datos, para obtener todos los usuarios. ElUser[]resultante se devuelve al cliente, pero solo con los campos que haya pedido y que estén definidos en los tipos GraphQL.Por ejemplo, si el cliente envía esta consulta:
En este ejemplo tenemos más campos disponibles, pero el cliente solo quiere
nameyemail. Esto se gestiona en base a nuestras definiciones de tipos GraphQL. Apollo se encarga de la magia.Estas mismas reglas aplican a cada consulta y mutación en **src/resolvers/index.ts**.
En la definición de
createUservimos que el cliente puede pasarname,emailyage. En el resolver hacemos lo mismo y llamamos acreateUseren nuestro servicio de base de datos. La respuesta se devuelve al cliente con los campos que haya solicitado.Compila y ejecuta la API GraphQL
Llegados a este punto, la API GraphQL debería estar en muy buen estado. Hay varias formas de compilar y ejecutar la aplicación.
Por ejemplo, puedes añadir lo siguiente a tu **package.json**:
Si optas por esta vía, ejecuta
npm run buildpara compilar el proyecto ynpm run startpara servirlo. El puerto por defecto es el 3000, y puedes probarlo con cURL o la herramienta que prefieras.Conclusión
Has visto cómo crear una API GraphQL con TypeScript y MongoDB como base de datos. Aunque el ejemplo es sencillo, hemos construido consultas y mutaciones para realizar operaciones CRUD contra la base. MongoDB encaja a la perfección en una API GraphQL porque el formato de los datos es similar de principio a fin. Por ejemplo, el cliente envía una consulta con apariencia de JSON. Los datos que viajan por la consulta pueden usarse con un formateo mínimo, y lo mismo al enviarlos a la base y recibirlos de ella. La experiencia es fluida y te permite centrarte en la experiencia de usuario y no en manipular datos para ajustarlos a la base.
Si te has atascado en algún punto, no dudes en revisar el proyecto final en GitHub.