Skip to content

srealmoreno/api-express

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

# # # # # #

Crear una api utilizando express y mysql sobre docker

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.

Indíce


Conocimientos preliminares

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


Requisitos

  • 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

Dependencias

  1. Git
  2. Nodejs
  3. npm
  4. Docker

Instalación de dependencias

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

Instalar extensiones en vscode

En visual studio code debes de buscar e instalar las siguientes extensiones

  • ms-azuretools.vscode-docker

Instalar extensión Docker en vscode

  • cweijan.vscode-mysql-client2

Instalar extensión Mysql en vscode

  • rangav.vscode-thunder-client

Instalar extensión Thunder cliente en vscode


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:

Ir a rama finished

Iniciando el proyecto

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"
}

Añadir variables de entorno

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

Comprobar conexión con docker

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

Comprobar conexión con imagen de docker


Mysql

Crear conexión mysql en vscode

En el apartado de EXPLORER:DATABASE (icono del barril) debemos de crear una nueva conexión con mysql

Comprobar conexión con imagen de docker

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

Comprobar conexión con imagen de docker


Crear base de datos y usuario en mysql

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;

Ejecutar script sql

En vscode vamos a presionar las teclas Ctrl + Shift + P, escribiremos Change active database y escogeremos mysql_root#mysql

Cambiar base de datos activa

Presionar las las teclas Ctrl + Shift + Enter para ejecutar el script

Cambiar base de datos activa

Utilizar base de datos creada

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


Módulo de mysql

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?


Index.js

El archivo index.js es nuestro archivo principal, en este declaramos nuestra API. abre el archivo index.js y sigue los siguientes pasos:


1. Importación de módulos y declaración de constantes

  • 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()?


2. Declaración de formato de mensajes de respuesta

  • 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)
}

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()?


3. Declaración de métodos de comprobación

  • 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?


4. Manejo de excepciónes de la base de datos

  • 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

5. Declaración de métodos HTTP en nuestro servidor

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:

  1. string: Path URI
  2. método: Manejador del método HTTP arrow Functions

Todo manejador (handler) recibirá por lo menos 2 argumentos:

  1. req Un Objeto de tipo express.request
  2. res Un Objeto de tipo express.request

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?


6. Poner en escucha el servidor

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}`))

Código fuente completo

Package json: package.json
Script sql: db.sql
Módulo de la base de datos: db.js
Archivo principal: index.js


Poner en marcha nuestro servidor

En vscode vamos a presionar las teclas Ctrl + Shift + P, luego escribir 'Alternar Terminal'

Ejecutamos lo siguiente en la terminal:

npm start

Abrir terminal integrada

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

Ocultar terminal integrada


Pruebas

Ahora que tenemos nuestro servidor en funcionamiento debemos hacer unas pruebas.


Crear colección en Thunder Client

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

Nueva colección


Crear solicitud GET

En nuestra colección crearemos un nuevo request con nombre Obtener Usuarios

Nueva solicitud

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

GET

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

Tests

Algunas dudas que quizás tengas:
¿Que es ResponseCode?
¿Que es ContentType?
¿Qué es json.success y json.status?


Crear solicitud POST

Duplica el request anterior y cambiale el nombre a Guardar Usuario Solicitud GET

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

Solicitud POST


Crear solicitud PUT

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

Solicitud PUT


Crear solicitud DELETE

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:

Solicitud POST

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

Solicitud DELETE


Al final debería verse así:

Previsualización de collección


Colección completa de Thunder Client

También puedes descargar e importar la colección que hicimos en el paso anterior

Importar colección

Ejecución de pruebas

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.


Ejecución solicitud POST

Ejecución solicitud POST


Ejecución solicitud GET

Aquí veremos el usuario que se guardo en el test anterior

Ejecución solicitud GET


Ejecución solicitud PUT

Aquí modificaremos el usuario que se guardó

Ejecución solicitud PUT

Para comprobarlo podemos a volver a ejecutar la primer prueba

Ejecución solicitud PUT


Ejecución solicitud DELETE

Ejecución solicitud DELETE


Wiki

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:

Structure of Json
JsonAPI

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


Autor

Srealmoreno srealmoreno

También puedes mirar la lista de todos los contribuyentes quíenes han participado en este proyecto.


Licencia

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

#