En este pequeño tutorial crearemos una api de usuarios utilizando Nodejs con express en Ubuntu 20.04 LTS. Por razones de entendimiento algunas cosas muy puntuales las dejaré en inglés y me limitaré a dar una explicación breve ya que este tutorial se haría muy extenso.
- Conocimientos preliminares
- Requisitos
- Dependencias
- Iniciando el proyecto
- Añadir variables de entorno
- Mysql
- Index.js
- Código fuente completo
- Poner en marcha nuestro servidor
- Pruebas
Necesitamos tener conocimientos acerca de algunos conceptos claves que utilizaremos en este proyecto:
Si tienes conocimientos de estos temas, puedes omitir la lectura.
Puedes leerlos en la WIKI del proyecto
- Distribución Debian, Ubuntu o derivados
- Conocimientos básicos de operaciones CRUD en mysql
- Conocimientos básicos HTTP/1.1
- Conocimientos básicos de JavaScript (EcmaScript 6)
- Visual studio code
- Git
- Nodejs
- npm
- Docker
El script se encargara de instalar las dependencias necesarias para el proyecto, en una terminal ejecutar lo siguiente:
sudo apt install git
git clone https://github.com/srealmoreno/api-express.git
cd api-express
chmod +x build/build.sh
sudo build/build.sh
En visual studio code debes de buscar e instalar las siguientes extensiones
- ms-azuretools.vscode-docker
- cweijan.vscode-mysql-client2
- rangav.vscode-thunder-client
Si deseas ahorrarte la creación del código fuente del proyecto, ejecuta esto en la terminal
git checkout finished
npm install
Luego sigue solamente estos pasos:
- Crear conexión mysql en vscode
- Ejecutar script sql
- Poner en marcha nuestro servidor
- Importar Tests Thunder en vscode (puedes encontrar el archivo en 'tests/thunder_test.json')
- Ejecución de Test
Debemos inicializar un proyecto utilizando npm, instalar dependencias y crear los archivos db.sql db.js index.js .env, en la terminal que tenemos abierta ejecutar lo siguiente:
npm init -y
npm install express mysql dotenv -E
npm install nodemon -E -D
touch db.sql db.js index.js .env
code .
Modificar el archivo package.json, agregar la linea "start" y "dev"
"scripts": {
"start": "node index.js",
"dev": "nodemon node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
}
Debemos definir algunas variables de entorno que necesitará nuestro proyecto, agrega el siguiente código en el archivo .env
NODE_PORT=3000
NODE_IP=127.0.0.1
MYSQL_PORT=3306
MYSQL_HOST=127.0.0.1
MYSQL_USER=express_api
MYSQL_PASSWORD=express_api
MYSQL_DATABASE=express_api
Para poder obtener las variables debemos importar el módulo 'dotenv' en el archivo principal del proyecto (index.js) y accedemos a ellas escribiendo "process.env." antes del nombre de la variable, e.j
require('dotenv').config()
const IP = process.env.NODE_IP
const PORT = process.env.NODE_PORT
En el apartado de docker (icono de la ballena) debemos de tener un contenedor mysql:8.0.25 node-mysql-api como la siguiente imagen
En el apartado de EXPLORER:DATABASE (icono del barril) debemos de crear una nueva conexión con mysql
Ingresar rellenar con los siguientes campos:
La contraseña es tu nombre de usuario. e.j: mi nombre de usuario es "srealmoreno" entonces la contraseña es srealmoreno
Connection Name | mysql_root |
Connection Target | global |
Database Type | mysql |
Host | 127.0.0.1 |
Port | 3306 |
Username | root |
Password | tu usuario |
Databases | |
Include Databases | |
ConnectTimeout | 5000 |
RequestTimeout | 10000 |
Timezone | +00:00 |
SSH Tunnel | desactivado |
Use SSL | desactivado |
Debemos crear un script sql que contenga las instrucciones para crear nuestra base de datos y nuestro usuario
En este tutorial crearemos una base de datos de usuarios, agrega el siguiente código al archivo db.sql
-- Crear base de datos solo si no existe llamada 'express_api'
CREATE DATABASE IF NOT EXISTS express_api;
-- Usar la base de datos anteriormente creada
USE express_api;
-- Crear tabla si no existe llamada 'usuarios' con los siguientes atributos:
-- 1. 'id' de tipo entero sin signo, que sea una llave primaria y auto incrementable
-- 2. 'nombre' de tipo varchar con longitud de 30 caracteres y que no sea NULL
-- 3. 'apellido' de tipo varchar con longitud de 30 caracteres y que no sea NULL
-- 4. 'correo' de tipo varchar con longitud de 40 caracteres
CREATE TABLE IF NOT EXISTS usuarios (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(30) NOT NULL,
apellido VARCHAR(30) NOT NULL,
correo VARCHAR(50)
);
-- Crear Usuario si no existe llamado 'express_api' y que
-- sea accesible desde cualquier parte con contraseña nativa 'express_api'
CREATE USER IF NOT EXISTS 'express_api' @'%' IDENTIFIED WITH mysql_native_password BY 'express_api';
-- Otorgarle permisos totales (CREATE, DROP, DELETE, INSERT, SELECT, UPDATE)
-- al usuario 'express_api' sobre la tabla anteriormente creada 'usuarios'
GRANT ALL PRIVILEGES on usuarios TO 'express_api' @'%';
-- Nota: si deseas utilizar el usuario solo en localhost, cambia
-- el % por la palabra localhost en las dos consultas anteriores
-- Recargar todos los permisos para actualizar el que hemos creado anteriormente
FLUSH PRIVILEGES;
En vscode vamos a presionar las teclas Ctrl + Shift + P, escribiremos Change active database y escogeremos mysql_root#mysql
Presionar las las teclas Ctrl + Shift + Enter para ejecutar el script
Ahora que tenemos la base de datos y el usuario creado, crear nueva conexión con los siguientes valores:
Connection Name | mysql_api |
Connection Target | global |
Database Type | mysql |
Host | 127.0.0.1 |
Port | 3306 |
Username | express_api |
Password | express_api |
Databases | |
Include Databases | express_api |
ConnectTimeout | 5000 |
RequestTimeout | 10000 |
Timezone | +00:00 |
SSH Tunnel | desactivado |
Use SSL | desactivado |
Ahora debemos cambiar la base de datos activa:
Ctrl + Shift + P, escribiremos Change active database y escogeremos mysql_api#express_api
agrega el siguiente código al archivo db.js:
const { promisify } = require("util")
const { createPool } = require("mysql")
const credenciales = {
port: process.env.MYSQL_PORT,
host: process.env.MYSQL_HOST,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE
}
const pool = createPool(credenciales)
pool.queryPromisify = promisify(pool.query)
module.exports = pool
Algunas dudas que quizás tengas:
¿Por qué las variables se declaran con {}?
¿Qué significa pool.queryPromisify en el código?
¿Qué significa process.env?
¿Qué es promisify en Nodejs?
¿Cúal es la diferencia entre utilizar un pool y una conexión a la base de datos?
El archivo index.js es nuestro archivo principal, en este declaramos nuestra API. abre el archivo index.js y sigue los siguientes pasos:
- Importamos el módulo de express y nuestro módulo de la conexión a la base de datos.
- Declaramos una dirección IP y un Puerto donde estará escuchando nuestra aplicación.
- Indicarle a express que decodifique los datos que nos envían a través de una solicitud HTTP
Agrega el siguiente código al archivo index.js
require('dotenv').config()
const express = require("express")
const database = require("./db.js")
const { urlencoded, json } = express
const app = express()
const { NODE_IP: IP, NODE_PORT: PORT } = process.env
app.use(urlencoded({ extended: false }))
app.use(json())
Algunas dudas que quizás tengas:
¿Por qué las variables se declaran con {}?
¿Qué significa process.env?
¿Que significa urlencoded() y json()?
-
Declaramos 2 nuevos métodos para los objetos de tipo response que enviarán un JSON con la estructura de nuestros mensajes
-
Para el caso de éxito
Agrega el siguiente código al archivo index.js
express.response.sendMessage = function (data = undefined) {
const body = {
success: true,
status: "success"
}
if (data !== undefined)
body["data"] = data
this.status(200).json(body)
}
- Para el caso de error
Agrega el siguiente código al archivo index.js
express.response.sendError = function (code, message) {
const body = {
success: false,
status: "failed",
error: {
code: code,
message: message
}
}
this.status(code).json(body)
}
Algunas dudas que quizás tengas:
¿Que significa response?
¿Que significa status()?
- Declaramos un nuevo método para los objetos de tipo request que comprobarán si los datos que nos envían a través de un POST o PUT están vacíos
Agrega el siguiente código al archivo index.js
express.request.isBodyEmpty = function () {
return Object.keys(this.body).length === 0
}
Algunas dudas que quizás tengas:
¿Que significa request?
- Declaramos un objeto mysqlErrors que contiene un método para manejar una excepción especifica.
Agrega el siguiente código al archivo index.js
const mysqlErrors = {
ER_NO_DEFAULT_FOR_FIELD: (res, errorMessage) => {
res.sendError(400, "Bad request, " + errorMessage)
},
ER_BAD_FIELD_ERROR: (res, errorMessage) => {
res.sendError(400, "Bad request, " + errorMessage)
},
ER_DUP_ENTRY: (res, errorMessage) => {
res.sendError(409, "Conflict, " + errorMessage)
}
}
Error | Descripción |
---|---|
ER_NO_DEFAULT_FOR_FIELD | Este error ocurre cuando omitimos un campo y este está declarado como NOT NULL y además NO tiene un valor por defecto |
ER_BAD_FIELD_ERROR | Este error ocurre cuando se desea asignar un campo desconocido a una tabla |
ER_DUP_ENTRY | Este error ocurre cuando se desea actualizar un ID único y otro registro ya lo tiene asignado |
Para declarar los métodos HTTP debemos de especificarlo con un . seguido de el tipo de método.
Cada método HTTP debe de tener 2 argumentos:
- string: Path URI
- método: Manejador del método HTTP arrow Functions
Todo manejador (handler) recibirá por lo menos 2 argumentos:
Para acceder a las variables de los Objetos tipo request tenemos 3 maneras diferentes.
Manera | Descripción | Método HTTP | Ejemplo |
---|---|---|---|
req.body.id | Variable en los datos HTTP | POST|PUT | Enviar un formulario HTML con el método POST |
req.query.id | Variable en la URL | ANY | /api/usuario?id=1 |
req.params.id | Variable en la URI | ANY | /api/usuario/:id |
Nuestra URI será "/api/usuarios"
Agrega el siguiente código al archivo index.js
// Obtener todos los usuarios
app.get("/api/usuarios", async (req, res) => {
console.log("Obtener todos los usuarios")
try {
const usuarios = await database.queryPromisify("SELECT * FROM usuarios")
if (usuarios.length === 0)
return res.sendError(404, "Empty list")
return res.sendMessage(usuarios)
} catch (error) {
console.error(error)
return res.sendError(500, "Internal Server Error, " + error.message)
}
})
// Obtener usuario por un ID pasado por la URL (req.params)
app.get("/api/usuarios/:id", async (req, res) => {
console.log("Obtener usuario por id " + req.params.id)
try {
const [usuario] = await database.queryPromisify(
"SELECT * FROM usuarios WHERE id = ?", [req.params.id]
)
if (usuario === undefined)
return res.sendError(404, "Not found")
return res.sendMessage(usuario)
} catch (error) {
console.error(error)
return res.sendError(500, "Internal Server Error, " + error.message)
}
})
Algunas dudas que quizás tengas:
¿Que significa res y req?
¿Qué significa aync y await en el código?
¿Qué significa ? y el array en la consulta?
Agrega el siguiente código al archivo index.js
// Guardar un nuevo usuario (req.body)
app.post("/api/usuarios", async (req, res) => {
console.log("Guardar un nuevo usuario")
if (req.isBodyEmpty())
return res.sendError(400, "Bad request, empty data")
try {
await database.queryPromisify("INSERT INTO usuarios SET ?", [req.body])
return res.sendMessage()
} catch (error) {
console.error(error)
if (mysqlErrors[error.code])
return mysqlErrors[error.code](res, error.message)
else
return res.sendError(500, "Internal Server Error, " + error.message)
}
})
error.code: es un String, es el nombre del error
Comprobamos si el error existe en nuestro objeto mysqlErrors. Si existe, ejecutamos el método asociado a ese error. Si no existe, enviamos el error "Internal Server Error"
Algunas dudas que quizás tengas:
¿Que significa res y req?
¿Qué significa aync y await en el código?
¿Qué significa SET en la consulta?
¿Qué significa ? y el array en la consulta?
Agrega el siguiente código al archivo index.js
// Actualizar usuario por un ID pasado por la URL
app.put("/api/usuarios/:id", async (req, res) => {
console.log("Actualizar usuario por id " + req.params.id)
if (req.isBodyEmpty())
return res.sendError(400, "Bad request, empty data")
try {
const { affectedRows } = await database.queryPromisify(
"UPDATE usuarios SET ? WHERE id = ?", [req.body, req.params.id]
)
if (affectedRows !== 0)
return res.sendMessage()
else
return res.sendError(404, "Not found")
} catch (error) {
console.error(error)
if (mysqlErrors[error.code])
return mysqlErrors[error.code](res, error.message)
else
return res.sendError(500, "Internal Server Error, " + error.message)
}
})
affectedRows: (Filas afectadas) es un Int que nos indicará la cantidad de registros que fueron actualizados.
Si esa variable es igual a 0, quiere decir que no existe ningún registro con el ID pasado por parámetro.
error.code: es un String, es el nombre del error
Comprobamos si el error existe en nuestro objeto mysqlErrors. Si existe, ejecutamos el método asociado a ese error. Si no existe, enviamos el error "Internal Server Error"
Algunas dudas que quizás tengas:
¿Que significa res y req?
¿Por qué la variable affectedRows se declara entre {}?
¿Qué significa aync y await en el código?
¿Qué significa SET en la consulta?
¿Qué significa ? y el array en la consulta?
Agrega el siguiente código al archivo index.js
// Eliminar un usuario por un ID pasado por la URL (req.params)
app.delete("/api/usuarios/:id", async (req, res) => {
console.log("Eliminar usuario por id " + req.params.id)
try {
const { affectedRows } = await database.queryPromisify(
"DELETE FROM usuarios WHERE id = ?", [req.params.id]
)
if (affectedRows !== 0)
return res.sendMessage()
else
return res.sendError(404, "Not found")
} catch (error) {
console.error(error)
return res.sendError(500, "Internal Server Error, " + error.message)
}
})
affectedRows: (Filas afectadas) es un Int que nos indicará la cantidad de registros que fueron eliminados.
Si esa variable es igual a 0, quiere decir que no existe ningún registro con el ID pasado por parámetro.
Algunas dudas que quizás tengas:
¿Que significa res y req?
¿Por qué la variable affectedRows se declara entre {}?
¿Qué significa aync y await en el código?
¿Qué significa SET en la consulta?
¿Qué significa ? y el array en la consulta?
Debemos de indicarle al servidor que este en modo listening en el puerto y dirección ip declarada en el paso 1 y además cuando llegue una ruta desconocida, enviará el el código 404 not found
Agrega el siguiente código al archivo index.js
app.use((req, res) => {
res.sendError(404, "Not found, " + req.path)
})
app.listen(PORT, IP, () => console.log(`Listening in http://${IP}:${PORT}`))
Package json: package.json
Script sql: db.sql
Módulo de la base de datos: db.js
Archivo principal: index.js
En vscode vamos a presionar las teclas Ctrl + Shift + P, luego escribir 'Alternar Terminal'
Ejecutamos lo siguiente en la terminal:
npm start
Si deseamos que nuestro servidor se reinicie automáticamente cada vez que modifiquemos cualquier archivo del proyecto ejecutamos
npm run dev
Si podemos ver el texto 'Listening in http://127.0.0.1:3000' quiere decir que nuestro servidor inició correctamente.
Ahora debemos ocultar la terminal haciendo click en el botón X
Ahora que tenemos nuestro servidor en funcionamiento debemos hacer unas pruebas.
Una colección es un grupo de solicitudes.
En el apartado de Thunder Client (icono de rayo) debemos crear una nueva colección con nombre Test node-api
En nuestra colección crearemos un nuevo request con nombre Obtener Usuarios
Rellena con los siguientes datos:
url | http://127.0.0.1:3000/api/usuarios |
Método | GET |
En la pestaña de Headers rellena con los siguientes datos:
Accept | application/json |
En la pestaña de Tests rellena con los siguientes datos:
ResponseCode | Equal | 200 |
ContentType | Equal | application/json; charset=utf-8 |
json.success | Equal | true |
json.status | Equal | sucess |
y por último Ctrl + S para guardar
Algunas dudas que quizás tengas:
¿Que es ResponseCode?
¿Que es ContentType?
¿Qué es json.success y json.status?
Duplica el request anterior y cambiale el nombre a Guardar Usuario
Rellena con los siguientes datos:
Método | POST |
En la pestaña de Body, escogeremos la opción Form-encodec y rellena con los siguientes datos:
nombre | Tu nombre |
apellido | Tu apellido |
correo | Tu correo |
y por último Ctrl + S para guardar
Duplica el request Guardar Usuario y cambiale el nombre a Actualizar Usuario
Rellena con los siguientes datos:
Recuerda que en este método debemos de indicarle el ID por la URL en este caso el ID será: 1
url | http://127.0.0.1:3000/api/usuarios/1 |
Método | PUT |
En la pestaña de Body, escogeremos la opción Form-encodec y rellena con los siguientes datos:
nombre | Otro nombre |
apellido | Otro apellido |
correo | Otro correo |
y por último Ctrl + S para guardar
Duplica el request Obtener Usuarios y cambiale el nombre a Eliminar Usuario
Para tener un orden arrastras el request con el mouse de la siguiente manera:
Rellena con los siguientes datos:
Recuerda que en este método debemos de indicarle el ID por la URL en este caso el ID será: 1
url | http://127.0.0.1:3000/api/usuarios/1 |
Método | DELETE |
y por último Ctrl + S para guardar
Al final debería verse así:
También puedes descargar e importar la colección que hicimos en el paso anterior
Para la ejecución de las pruebas es simple solo debemos ir a cada una de las solicitudes y hacer click en el botón Send
Cuando ejecutemos las pruebas tendremos 2 pestañas importantes:
Response: Es el Json con que responderá el servidor
Test Results: Serán el resultado de las pruebas, debemos obtener Pass en el resultado de cada prueba
Para no obtener el error "Empty List" primero vamos a guardar un nuevo usuario.
Aquí veremos el usuario que se guardo en el test anterior
Aquí modificaremos el usuario que se guardó
Para comprobarlo podemos a volver a ejecutar la primer prueba
HTTP:
HTTP Messages and Structure
HTTP Request Methods
HTTP Status Codes
JavaScript:
Destructuring assignment
Arrow Functions
Monkey patch
Base de datos:
Express mysql
Pool Connection
Mysql pool
Promisify in Node
Mysql error codes
Sql Injection
Sql Injection in Nodejs
Json:
Docker:
Install
Docker Pull
Docker Image
Docker Run
Docker Start
Docker Stop
Docker mysql
Express:
Express api
Express json
Express urlencoded
Express request body
Express request query
Express request param
Express GET Method
Express POST Method
Express PUT Method
Express DELETE Method
Express Response Status
Extensiones vscode:
Docker
Database Client
Thunder Client
srealmoreno |
---|
También puedes mirar la lista de todos los contribuyentes quíenes han participado en este proyecto.
Este proyecto está bajo la Licencia GNU General Public License v3.0 - mira el archivo LICENSE para más detalles
Programación en Android - Salvador real