José Navarro es un desarrollador full stack en FAMOCO en Bruselas, Bélgica. Ha estado trabajando durante los últimos 3 años como desarrollador web. desarrollador con Node.js, Java, AngularJS y ReactJS, y tiene un profundo interés en el desarrollo web y las tecnologías móviles.
Introducción
Vamos a desarrollar una API REST usando Node.js y Couchbase ODM Ottoman. Hay unos cuantos frameworks para hacer esto en Node.js, así que vamos a usar hapi.js, que facilita el inicio y desarrollo de una API, y su código es limpio y fácil de entender. También proporciona un validador en la petición para que podamos integrarnos bien con el modelo Ottoman, que vamos a utilizar para poder abstraer nuestro código y trabajar con objetos.
Requisitos
Para construir el proyecto, necesitas tener instalado lo siguiente en tu ordenador:
-
Node.js y NPM
-
Servidor Couchbase
Servidor Hapi
Primero, creamos el directorio principal para nuestro proyecto, luego vamos dentro de ese directorio e iniciamos el proyecto npm, donde se nos pedirán algunos parámetros para nuestro proyecto.
Podemos hacerlo con los siguientes comandos:
mkdir nodo–hapi–couchbase–api
npm init
El siguiente paso es añadir las dependencias a nuestro proyecto. En primer lugar, añadiremos las dependencias hapi.js después añadimos los paquetes relacionados con Couchbase y, por último, añadimos nodemon a nuestras dependencias dev para la recarga en vivo de nuestro servidor mientras estamos codificando.
npm instalar –S hapi joi
npm instalar –S sofá base otomana
npm instalar –D nodemon
Una vez que todo esto está listo, empezamos a crear nuestro proyecto. Creamos una carpeta src donde tendremos todo nuestro código. Dentro creamos un index.js donde tendremos nuestro servidor hapi básico. Allí añadimos el siguiente código:
const Hapi = requiere(hapi);
// Crear un servidor con un host y un puerto
const servidor = nuevo Hapi.Servidor();
servidor.conexión({
host: "localhost",
puerto: 5000,
rutas: {
cors: cierto,
}
});
// Iniciar el servidor
servidor.iniciar( err => {
si( err ) {
// Gestión de errores de lujo aquí
consola.error( err );
tirar err;
}
consola.registro( Servidor iniciado en ${ server.info.uri } );
} );
módulo.exportaciones = servidor;
Acabamos de crear nuestro servidor básico.
Ahora vamos a definir una ruta de entrada para nuestro servidor. Primero, creamos una carpeta API donde definiremos nuestras rutas. Y creamos un archivo index.jscon el código de nuestra ruta de entrada:
const rutas = [
{
método: 'GET',
ruta: ‘/’,
config: {
manipulador: (solicitar, respuesta) => {
devolver respuesta({
nombre: 'node-hapi-couchbase-api',
versión: 1
});
}
}
}
];
módulo.exportaciones = rutas;
En el principal index.js vamos a importar las rutas. Para ello añadimos el siguiente código antes del código server.start que definimos anteriormente:
const rutas = requiere('./api');
// Añadir las rutas
servidor.ruta(rutas);
Ahora en nuestro paquete.json añadiremos el archivo script sección.
"scripts": {
"inicio": "nodemon ./src/index.js"
},
Si ejecutamos npm iniciariniciaremos nuestro servidor. Podemos comprobarlo yendo a http://localhost:5000y deberíamos recibir una respuesta.
{"nombre":"node-hapi-couchbase-api","version":1}
Conector de base de datos
Para configurar el conector de base de datos, vamos a crear una carpeta db donde almacenaremos la información de la base de datos y la lógica del conector.
Vamos a almacenar la información en el archivo config.json con el siguiente código:
{
"couchbase": {
"punto final": "localhost:8091",
"cubo": "api"
}
}
Para el conector, vamos a crear un archivo index.jsdonde vamos a importar el fichero de configuración y la librería Couchbase e inicializar la conexión con la base de datos y el bucket.
dejar config = requiere('./config');
dejar couchbase = requiere(couchbase);
deje endpoint = config.couchbase.punto final;
dejar cubo = config.couchbase.cubo;
deje miCluster = nuevo couchbase.Grupo(punto final, función(err) {
si (err) {
consola.registro("No se puede conectar a couchbase: %s", err);
}
consola.registro('conectado a db %s', punto final);
});
dejar miCubo = miCluster.openBucket(cubo, función(err) {
si (err) {
consola.registro("No se puede conectar al cubo: %s", err);
}
consola.registro('conectado al cubo %s', cubo);
});
El siguiente paso es importar el Otomano ODM de Couchbase y configurarlo con el cubo.
dejar otomana = requiere(otomana);
otomana.tienda = nuevo otomana.CbStoreAdapter(miCubo, couchbase);
Por último, vamos a exportar el cubo y el otomano para tener acceso desde otros archivos.
módulo.exportaciones = {
cubo: miCubo,
otomana: otomana
};
Modelos
Ahora que tenemos nuestro servidor básico funcionando, vamos a definir nuestros modelos con Ottoman. Vamos a definir dos modelos: uno para un Usuario y otro para un Publicar en. Para ello creamos una carpeta llamada modelosy dentro creamos dos archivos js: usuario.js y post.js. Podemos añadir las validaciones en el modelo, pero hapi.js ofrece una validación antes de manejar la ruta, así que vamos a usar eso para validar los datos que recibimos del usuario antes de pasarlos a nuestro modelo.
Modelo de usuario
El usuario dispondrá de tres campos: nombre, correo electrónico, y contraseña. Creamos nuestro modelo de usuario utilizando el paquete Ottoman. Nuestro modelo de usuario contiene el siguiente código:
dejar otomana = requiere('../db').otomana;
deje UserModel = otomana.modelo(Usuario, {
contraseña: 'cadena',
nombre: 'cadena',
correo electrónico: 'cadena',
}, {
índice: {
findByEmail: {
por: 'email',
tipo: refdoc
}
}
});
En primer lugar, importamos la instancia de Ottoman que iniciamos en el conector db. Después empezamos a definir nuestro modelo. El primer parámetro es el nombre de nuestro modelo, en este caso 'User'. El segundo parámetro es el objeto JSON que contiene el nombre del campo y el tipo; en nuestro caso todos los valores son string (comprueba Ottoman para ver otros tipos). El siguiente parámetro es el objeto que contiene el índice que queremos crear. Vamos a crear un índice para el correo electrónico de modo que podamos utilizar ese índice para consultar por el usuario utilizando nuestro modelo; esto también creará una restricción para evitar correos electrónicos duplicados en nuestros usuarios.
Cuando creamos un índice necesitamos llamar a la función ensureIndices para crear los índices internamente.
otomana.ensureIndices(función(err) {
si (err) {
devolver consola.error('Error ensure indices USER', err);
}
consola.registro('Asegurar índices USUARIO');
});
El último paso es exportar el modelo.
módulo.exportaciones = ModeloUsuario;
Modelo de puesto
El puesto contendrá cuatro campos: título y cuerpoEl marca de tiempoy el usuario.
En primer lugar, importamos la instancia de Ottoman que inicializamos en el conector db, y también importamos el modelo User.
dejar otomana = requiere('../db').otomana;
deje Usuario = requiere('./usuario');
deje PostModelo = otomana.modelo(Publicar, {
usuario: Usuario,
título: 'cadena',
cuerpo: 'cadena',
marca de tiempo: {
tipo: 'Fecha',
por defecto: Fecha.ahora
}
});
El primer parámetro es el nombre de nuestro modelo, 'Post'. El segundo es el objeto JSON con nuestro campo. En este caso definimos usuario con el tipo Usuario que definimos en nuestro modelo anterior; el título y el cuerpo de tipo cadenay marca de tiempo de tipo Fecha. Vamos a crear un valor por defecto con la marca de tiempo actual cuando se crea el objeto.
Y por último exportamos nuestro modelo.
módulo.exportaciones = PostModelo;
Rutas API
Vamos a definir nuestras rutas para Usuarios y Posts; la ruta básica que vamos a utilizar es /api/v1. En nuestro archivo index.js dentro de API, vamos a importar las rutas user y las rutas post, y las uniremos en un array.
const usuarios = requiere('./usuarios');
const puestos = requiere('./posts');
…
rutas = rutas.concat(usuarios);
rutas = rutas.concat(puestos);
En ambas rutas, User y Post, vamos a definir los métodos para realizar una operación CRUD. Para cada ruta, necesitamos definir el método, path, y config. En la sección config proporcionamos el handler, que es la función a realizar; y también podemos proporcionar una función validate que será llamada antes de realizar la función handle. Para las validaciones, vamos a utilizar el paquete Joi, que podemos utilizar para definir el esquema y las validaciones para el cuerpo de la petición.
Rutas de usuario
Para los usuarios, vamos a utilizar la ruta /api/v1/usuarios. El primer paso en nuestro archivo de rutas es importar el modelo de usuario y el paquete joi.
const Usuario = requiere('../modelos/usuario');
const Joi = requiere(joi);
Recuperar la lista de usuarios GET /api/v1/users
En la función handle vamos a utilizar la función find del modelo User que nos permite consultar la db para recoger todos los documentos de tipo User.
{
método: 'GET',
ruta: '/api/v1/users',
config: {
manipulador: (solicitar, respuesta) => {
Usuario.encontrar({}, (err, usuarios) => {
si (err) {
devolver respuesta({
estado: 400,
mensaje: err.mensaje
}).código(400);
}
devolver respuesta({
datos: usuarios,
cuente: usuarios.longitud
});
});
}
}
}
Vamos a devolver un objeto con un array de usuario y un count con el número de objetos dentro del array.
Recuperar un usuario por su id GET /api/v1/users/{id}
En este caso, vamos a consultar un usuario por su id de documento, por lo que vamos a utilizar la función incorporada getById en nuestro modelo para recuperar un documento de la base de datos.
En este caso, proporcionamos un valide para validar que el valor param id es una cadena.
{
método: 'GET',
ruta: '/api/v1/users/{id}',
config: {
manipulador: (solicitar, respuesta) => {
Usuario.getById(solicitar.parámetros.id, (err, usuario) => {
si (err) {
devolver respuesta({
estado: 400,
mensaje: err.mensaje
}).código(400);
}
devolver respuesta(usuario);
});
},
valide: {
parámetros: {
id: Joi.cadena(),
}
}
}
}
Vamos a devolver el documento del usuario.
Crear un nuevo usuario POST /api/v1/users
Ahora vamos a crear un nuevo usuario. El primer paso es crear el Usuario con el modelo de Usuario y el cuerpo de la petición.
Proporcionamos un objeto validate para comprobar ese payload (cuerpo de la petición).
{
método: 'POST',
ruta: '/api/v1/users',
config: {
manipulador: (solicitar, respuesta) => {
const usuario = nuevo Usuario(solicitar.carga útil);
usuario.guardar((err) => {
si (err) {
devolver respuesta({
estado: 400,
mensaje: err.mensaje
}).código(400);
}
devolver respuesta(usuario).código(201);
});
},
valide: {
carga útil: {
contraseña: Joi.cadena().alphanum().min(3).max(30).obligatorio(),
correo electrónico: Joi.cadena().correo electrónico().obligatorio(),
nombre: Joi.cadena()
}
}
}
}
Devolveremos el objeto del nuevo usuario creado.
Actualizar un usuario PUT /api/v1/users/{id}
Ahora vamos a actualizar un usuario. En este caso primero vamos a recuperar el documento del usuario de la base de datos, luego actualizaremos los campos, y finalmente guardaremos el documento actualizado en la base de datos.
En este caso, proporcionamos un objeto validate donde validamos tanto params como payload.
{
método: 'PUT',
ruta: '/api/v1/users/{id}',
config: {
manipulador: (solicitar, respuesta) => {
Usuario.getById(solicitar.parámetros.id, (err, usuario) => {
si (err) {
devolver respuesta({
estado: 400,
mensaje: err.mensaje
}).código(400);
}
const carga útil = solicitar.carga útil;
si (carga útil.nombre) {
usuario.nombre = carga útil.nombre;
}
si (carga útil.contraseña) {
usuario.contraseña = carga útil.contraseña;
}
usuario.guardar((err) => {
si (err) {
devolver respuesta({
estado: 400,
mensaje: err.mensaje
}).código(400);
}
devolver respuesta(usuario).código(200);
});
});
},
valide: {
parámetros: {
id: Joi.cadena(),
},
carga útil: {
nombre: Joi.cadena(),
contraseña: Joi.cadena().alphanum().min(3).max(30),
}
}
}
}
Le devolveremos el documento actualizado.
Eliminar un usuario DELETE /api/v1/users/{id}
En este caso vamos a eliminar un usuario. Primero recuperamos el documento de la base de datos y luego lo eliminamos.
{
método: 'DELETE',
ruta: '/api/v1/users/{id}',
config: {
manipulador: (solicitar, respuesta) => {
Usuario.getById(solicitar.parámetros.id, (err, usuario) => {
si (err) {
devolver respuesta({
estado: 400,
mensaje: err.mensaje
}).código(400);
}
usuario.eliminar((err) => {
si (err) {
devolver respuesta({
estado: 400,
mensaje: err.mensaje
}).código(400);
}
devolver respuesta(usuario);
});
});
},
valide: {
parámetros: {
id: Joi.cadena(),
}
}
}
}
Devolveremos el documento eliminado.
Por último, tenemos que exportar las rutas.
módulo.exportaciones = rutas;
Rutas postales
Para las rutas de post vamos a utilizar la ruta /api/v1/users/{userId}/posts, de forma que solo realicemos operaciones a post relacionadas con el usuario. Definiremos una función de validación que va a comprobar si el usuario existe en la base de datos, y lo devolverá para que tengamos acceso al usuario en la función que gestiona la petición.
La primera sección del código son las importaciones y esa función.
const Usuario = requiere('../modelos/usuario');
const Publicar en = requiere('../modelos/post');
const Joi = requiere(joi);
const validateUser = (valor, opciones, siguiente) => {
const userId = opciones.contexto.parámetros.userId;
Usuario.getById(userId, (err, usuario) => {
siguiente(err, Objeto.asignar({}, valor, { usuario }))
})
};
Recuperar la lista de mensajes del usuario GET /api/v1/users/{userId}/posts
Para recuperar todos los post del usuario vamos a utilizar el modelo Post y la función find. Vamos a ejecutar con un objeto donde vamos a proporcionar el id del usuario para recuperar todos los Post del usuario.
{
método: 'GET',
ruta: '/api/v1/users/{userId}/posts',
config: {
manipulador: (solicitar, respuesta) => {
const usuario = solicitar.consulta.usuario;
Publicar en.encontrar({ usuario: { _id: usuario._id } }, (err, puestos) => {
si (err) {
devolver respuesta({
estado: 400,
mensaje: err.mensaje
}).código(400);
}
devolver respuesta({
datos: puestos,
cuente: puestos.longitud
});
})
},
valide: {
consulta: validarUsuario,
}
}
}
Vamos a devolver un objeto con un array de posts y el recuento de posts.
Recuperar una entrada GET /api/v1/users/{userId}/posts/{postId}
Al igual que hacemos en la lista de entradas, para recuperar una entrada vamos a llamar a la función find con el id de usuario, y también con el id de la entrada que queremos recuperar.
{
método: 'GET',
ruta: '/api/v1/users/{userId}/posts/{postId}',
config: {
manipulador: (solicitar, respuesta) => {
const usuario = solicitar.consulta.usuario;
const postId = solicitar.parámetros.postId;
Publicar en.encontrar({ usuario: { _id: usuario._id }, _id: postId }, (err, puestos) => {
si (err) {
devolver respuesta({
estado: 400,
mensaje: err.mensaje
}).código(400);
}
si (puestos.longitud === 0) {
devolver respuesta({
estado: 404,
mensaje: No encontrado
}).código(404);
} si no {
devolver respuesta(puestos[0]);
}
})
},
valide: {
consulta: validarUsuario,
}
}
}
Vamos a devolver el primer mensaje que recibamos. Sólo podemos recibir una entrada porque estamos consultando la base de datos para encontrar una entrada por su id, por eso devolvemos el primer elemento del array. Si no recibimos ninguna entrada, significa que no hay ninguna entrada con ese id relacionada con ese usuario, por lo que devolvemos un error de no encontrado.
Crear una nueva entrada POST /api/v1/users/{userId}/posts
Para crear un post, vamos a hacer el mismo proceso que hicimos en el usuario. Proporcionamos un objeto validate para el payload para que podamos validar el cuerpo que recibimos. Sólo validamos el título y el cuerpo del post porque el usuario del que lo estamos obteniendo -la ruta y el timestamp- se genera cuando creamos el post.
En la función handler creamos un nuevo post con el payload, y establecemos el usuario del post con el user.
{
método: 'POST',
ruta: '/api/v1/users/{userId}/posts',
config: {
manipulador: (solicitar, respuesta) => {
const usuario = solicitar.consulta.usuario;
const Correo electrónico: = nuevo Publicar en(solicitar.carga útil);
Correo electrónico:.usuario = usuario;
Correo electrónico:.guardar((err) => {
si (err) {
devolver respuesta({
estado: 400,
mensaje: err.mensaje
}).código(400);
}
devolver respuesta(Correo electrónico:).código(201);
});
},
valide: {
consulta: validarUsuario,
carga útil: {
título: Joi.cadena().obligatorio(),
cuerpo: Joi.cadena().obligatorio(),
}
}
}
}
Devolveremos el puesto creado.
Actualizar una entrada PUT /api/v1/users/{userId}/posts/{postId}
Para actualizar un post, proporcionamos un objeto validate como hicimos en el create, y vamos a permitir cambiar el título y el cuerpo del post. Aquí vamos a consultar el post usando el modelo Post y la función getById, así que cuando recuperamos el post, comprobamos si el usuario coincide con el usuario proporcionado en la ruta. Si coincide, actualizamos los campos del post con los valores de la petición, y guardamos el post actualizado.
{
método: 'PUT',
ruta: '/api/v1/users/{userId}/posts/{postId}',
config: {
manipulador: (solicitar, respuesta) => {
Publicar en.getById(solicitar.parámetros.postId, (err, Correo electrónico:) => {
si (err) {
devolver respuesta({
estado: 400,
mensaje: err.mensaje
}).código(400);
}
si (solicitar.parámetros.userId === Correo electrónico:.usuario._id) {
const carga útil = solicitar.carga útil;
si (carga útil.título) {
Correo electrónico:.título = carga útil.título;
}
si (carga útil.cuerpo) {
Correo electrónico:.cuerpo = carga útil.cuerpo;
}
Correo electrónico:.guardar((err) => {
si (err) {
devolver respuesta({
estado: 400,
mensaje: err.mensaje
}).código(400);
}
devolver respuesta(Correo electrónico:).código(200);
});
} si no {
devolver respuesta({
estado: 401,
mensaje: "El usuario no puede editar el post"
}).código(401);
}
})
},
valide: {
consulta: validarUsuario,
carga útil: {
título: Joi.cadena().obligatorio(),
cuerpo: Joi.cadena().obligatorio(),
}
}
}
}
Devolveremos el post actualizado. Si el usuario no coincide con el usuario del post, recibiremos un error autorizado porque el usuario no es el propietario del post.
Eliminar una entrada DELETE /api/v1/users/{userId}/posts/{postId}
Como hicimos en la actualización, buscamos la entrada y comprobamos si el usuario de la ruta coincide con el usuario de la entrada. Si coinciden, procedemos y borramos el post.
{
método: 'DELETE',
ruta: '/api/v1/users/{userId}/posts/{postId}',
config: {
manipulador: (solicitar, respuesta) => {
Publicar en.getById(solicitar.parámetros.postId, (err, Correo electrónico:) => {
si (err) {
devolver respuesta({
estado: 400,
mensaje: err.mensaje
}).código(400);
}
si (solicitar.parámetros.userId === Correo electrónico:.usuario._id) {
Correo electrónico:.eliminar((err) => {
si (err) {
devolver respuesta({
estado: 400,
mensaje: err.mensaje
}).código(400);
}
devolver respuesta(Correo electrónico:).código(200);
});
} si no {
devolver respuesta({
estado: 401,
mensaje: "El usuario no puede borrar el post"
}).código(401);
}
})
},
valide: {
consulta: validateUser
}
}
}
Devolvemos el mensaje borrado.
Por último, exportamos las rutas.
módulo.exportaciones = rutas;
Prueba
Para probar la API, podemos hacerlo con Postman, cURL, o cualquier otra aplicación.
A continuación hemos creado algunos ejemplos cURL para probar la API. Los ids utilizados son los que hemos creado con las operaciones POST, así que cuando los ejecutes, recuerda cambiar la ruta para que coincida con los ids de los recursos que has generado.
# consultamos a los usuarios
rizo –X GET "http://localhost:5000/api/v1/users"
# creamos un usuario
rizo –X POST –H "Content-Type: application/json" –d ‘{
"nombre": "jose",
"contraseña": "jose",
"email": "jose.navarro@famoco.com"
}’ "http://localhost:5000/api/v1/users"
# deberíamos obtener un json con un usuario
rizo –X GET "http://localhost:5000/api/v1/users"
# deberíamos obtener los usuarios con ese id
rizo –X GET “http://localhost:5000/api/v1/users/e0b66baa-851d-4aae-9ef2-f12575519e5e”
# actualizamos el usuario
rizo –X PUT –H "Content-Type: application/json" –d ‘{
"name": "jose_update",
"contraseña": "joseedit"
}’ “http://localhost:5000/api/v1/users/e0b66baa-851d-4aae-9ef2-f12575519e5e”
# borramos el usuario
rizo –X BORRAR “http://localhost:5000/api/v1/users/e0b66baa-851d-4aae-9ef2-f12575519e5e”
# puestos
# buscamos el puesto de un usuario
rizo –X GET “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts”
# creamos un puesto
rizo –X POST –H "Content-Type: application/json" –d ‘{
"título": "título de mi post",
"body": "cuerpo de mi post"
}’ “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts”
# consultamos por un puesto
rizo –X GET “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts/94b1dd8e-73aa-4e4e-8b29-0870e6515945”
# actualizamos un puesto
rizo –X PUT –H "Content-Type: application/json" –d ‘{
"título": "mi título editado",
"cuerpo": "mi cuerpo editado"
}’ “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts/94b1dd8e-73aa-4e4e-8b29-0870e6515945”
# borramos un mensaje
rizo –X BORRAR “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts/94b1dd8e-73aa-4e4e-8b29-0870e6515945”
Conclusión
Como hemos visto, fue fácil desarrollar una API REST básica para realizar una operación CRUD, y el código es sencillo y fácil de leer. Y con Ottoman pudimos abstraer la lógica db para trabajar con objetos y los métodos que nos proporcionaba el ODM.