Cómo crear una aplicación de facturación ligera con Node: base de datos y API

Introducción
Una factura es un documento de bienes y servicios proporcionados que una empresa puede presentar a sus clientes.
Una herramienta de facturación digital deberá realizar un seguimiento de los clientes, registrar los servicios y los precios, actualizar el estado de las facturas pagadas y proporcionar una interfaz para visualizar las facturas. Esto requerirá CRUD (Crear, Leer, Actualizar, Eliminar), bases de datos y enrutamiento.
Nota: Esta es la Parte 1 de una serie de 3 partes. El segundo tutorial es Cómo crear una aplicación de facturación liviana con Node: Interfaz de usuario . El tercer tutorial es Cómo crear una aplicación de facturación liviana con Vue y Node: Autenticación JWT y envío de facturas .
En este tutorial, creará una aplicación de facturación con Vue y NodeJS . Esta aplicación realizará funciones como crear, enviar, editar y eliminar una factura.
Prerrequisitos
Para completar este tutorial, necesitarás:
- Node.js instalado localmente, lo cual puedes hacer siguiendo Cómo instalar Node.js y crear un entorno de desarrollo local .
- SQLite instalado localmente, lo cual puedes hacer siguiendo Cómo instalar y usar SQLite .
- Será necesario descargar e instalar una herramienta como Postman para probar los puntos finales de la API.
Nota: SQLite actualmente viene preinstalado en macOS y Mac OS X de forma predeterminada.
Este tutorial fue verificado con Node v16.1.0, npm
v7.12.1 y SQLite v3.32.3.
Paso 1: Configuración del proyecto
Ahora que tenemos todos los requisitos establecidos, el siguiente paso es crear el servidor backend para la aplicación. El servidor backend mantendrá la conexión a la base de datos.
Comience creando un directorio para el nuevo proyecto:
- mkdir invoicing-app
Navegue hasta el directorio del proyecto recién creado:
- cd invoicing-app
Luego inicialícelo como un proyecto Node:
- npm init -y
Para que el servidor funcione correctamente, hay algunos paquetes de Node que deben instalarse. Puede instalarlos ejecutando este comando:
- npm install bcrypt@5.0.1 bluebird@3.7.2 cors@2.8.5 express@4.17.1 lodash@4.17.21 multer@1.4.2 sqlite3@5.0.2^ umzug@2.3.0^
Ese comando instala los siguientes paquetes:
bcrypt
para codificar las contraseñas de los usuariosbluebird
Cómo usar Promesas al escribir migracionescors
para compartir recursos de origen cruzadoexpress
Para potenciar nuestra aplicación weblodash
para métodos de utilidadmulter
Para gestionar solicitudes de formulario entrantessqlite3
Para crear y mantener la base de datosumzug
como ejecutor de tareas para ejecutar nuestras migraciones de bases de datos
Nota: Desde la publicación original, este tutorial se actualizó para incluir lodash
. isEmpty()
La biblioteca de middleware para el manejo multipart/form-data
se cambió de connect-multiparty
a multer
.
Crea un server.js
archivo que contendrá la lógica de la aplicación. En el server.js
archivo, importa los módulos necesarios y crea una aplicación Express:
servidor.js
const express = require('express');const cors = require('cors');const sqlite3 = require('sqlite3').verbose();const PORT = process.env.PORT || 3128;const app = express();app.use(express.urlencoded({extended: false}));app.use(express.json());app.use(cors());// ...
Crea una /
ruta para probar que el servidor funciona:
servidor.js
// ...app.get('/', function(req, res) { res.send('Welcome to Invoicing App.');});
app.listen()
Le dice al servidor el puerto que debe escuchar para las rutas entrantes:
servidor.js
// ...app.listen(PORT, function() { console.log(`App running on localhost:${PORT}.`);});
Para iniciar el servidor, ejecute lo siguiente en el directorio de su proyecto:
- node server
Su aplicación ahora comenzará a escuchar las solicitudes entrantes.
Paso 2: Creación y conexión a la base de datos mediante SQLite
Para una aplicación de facturación, se necesita una base de datos para almacenar las facturas existentes. SQLite será el cliente de base de datos elegido para esta aplicación.
Comience creando una database
carpeta:
- mkdir database
Ejecute el sqlite3
cliente y cree un InvoicingApp.db
archivo para su base de datos en este nuevo directorio:
- sqlite3 database/InvoicingApp.db
Ahora que se ha seleccionado la base de datos, el siguiente paso es crear las tablas necesarias.
Esta aplicación utilizará tres tablas:
- “Usuarios”: aquí se incluirán los datos del usuario (
id
,name
,email
,company_name
,password
) - “Facturas”: almacena datos de una factura (
id
,name
,paid
,user_id
) - “Transacciones”: Transacciones singulares que se unen para formar una factura (
name
,price
,invoice_id
)
Una vez identificadas las tablas necesarias, el siguiente paso es ejecutar las consultas para crear las tablas.
Las migraciones se utilizan para realizar un seguimiento de los cambios en una base de datos a medida que la aplicación crece. Para ello, cree una migrations
carpeta en el database
directorio.
- mkdir database/migrations
Esta será la ubicación de todos los archivos de migración.
Ahora, crea un 1.0.js
archivo en la migrations
carpeta. Esta convención de nombres sirve para mantener un registro de los cambios más recientes.
En el 1.0.js
archivo, primero importa los módulos de nodo:
base de datos/migraciones 1.0.js
"use strict";const path = require('path');const Promise = require('bluebird');const sqlite3 = require('sqlite3');// ...
Luego, exporte una up
función que se ejecutará cuando se ejecute el archivo de migración y una down
función para revertir los cambios en la base de datos.
base de datos/migraciones/1.0.js
// ...module.exports = { up: function() { return new Promise(function(resolve, reject) { let db = new sqlite3.Database('./database/InvoicingApp.db'); db.run(`PRAGMA foreign_keys = ON`); // ...
En la up
función, primero se realiza la conexión a la base de datos. Luego, se habilitan las claves externas en la sqlite
base de datos. En SQLite, las claves externas están deshabilitadas de manera predeterminada para permitir la compatibilidad con versiones anteriores, por lo que las claves externas deben habilitarse en cada conexión.
A continuación, especifique las consultas para crear las tablas:
base de datos/migraciones/1.0.js
// ... db.serialize(function() { db.run(`CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT, email TEXT, company_name TEXT, password TEXT )`); db.run(`CREATE TABLE invoices ( id INTEGER PRIMARY KEY, name TEXT, user_id INTEGER, paid NUMERIC, FOREIGN KEY(user_id) REFERENCES users(id) )`); db.run(`CREATE TABLE transactions ( id INTEGER PRIMARY KEY, name TEXT, price INTEGER, invoice_id INTEGER, FOREIGN KEY(invoice_id) REFERENCES invoices(id) )`); }); db.close(); }); }}
La serialize()
función se utiliza para especificar que las consultas se ejecutarán secuencialmente y no simultáneamente.
Una vez creados los archivos de migración, el siguiente paso es ejecutarlos para realizar los cambios en la base de datos. Para ello, crea una scripts
carpeta desde la raíz de tu aplicación:
- mkdir scripts
Luego, crea un archivo llamado migrate.js
en este nuevo directorio y agrega lo siguiente al migrate.js
archivo:
scripts/migrate.js
const path = require('path');const Umzug = require('umzug');let umzug = new Umzug({ logging: function() { console.log.apply(null, arguments); }, migrations: { path: './database/migrations', pattern: /.js$/ }, upName: 'up'});// ...
En primer lugar, se importan los módulos de nodo necesarios. A continuación, umzug
se crea un nuevo objeto con las configuraciones. path
También pattern
se especifican los scripts de migración. Para obtener más información sobre las configuraciones, consulte el archivo umzug
README .
Para proporcionar también una retroalimentación detallada, cree una función para registrar eventos como se muestra a continuación y luego, finalmente, ejecute la up
función para ejecutar las consultas de base de datos especificadas en la carpeta de migraciones:
scripts/migrate.js
// ...function logUmzugEvent(eventName) { return function(name, migration) { console.log(`${name} ${eventName}`); };}// using event listeners to log eventsumzug.on('migrating', logUmzugEvent('migrating'));umzug.on('migrated', logUmzugEvent('migrated'));umzug.on('reverting', logUmzugEvent('reverting'));umzug.on('reverted', logUmzugEvent('reverted'));// this will run your migrationsumzug.up().then(console.log('all migrations done'));
Ahora, para ejecutar el script, ve a tu terminal y en el directorio raíz de tu aplicación, ejecuta:
- node scripts/migrate.js
Verá un resultado similar al siguiente:
Outputall migrations done== 1.0: migrating =======1.0 migrating
En este punto, la ejecución del migrate.js
script ha aplicado la 1.0.js
configuración a InvoicingApp.db
.
Paso 3: Creación de rutas de aplicaciones
Ahora que la base de datos está configurada adecuadamente, el siguiente paso es volver al server.js
archivo y crear las rutas de la aplicación. Para esta aplicación, se pondrán a disposición las siguientes rutas:
URL | MÉTODO | FUNCIÓN |
---|---|---|
/register |
POST |
Para registrar un nuevo usuario |
/login |
POST |
Para iniciar sesión con un usuario existente |
/invoice |
POST |
Para crear una nueva factura |
/invoice/user/{user_id} |
GET |
Para obtener todas las facturas de un usuario |
/invoice/user/{user_id}/{invoice_id} |
GET |
Para obtener una determinada factura |
/invoice/send |
POST |
Para enviar factura al cliente |
CORREO/register
Para registrar un nuevo usuario, se realizará una solicitud POST a la /register
ruta de su servidor.
Revise server.js
y agregue las siguientes líneas de código:
servidor.js
// ...const _ = require('lodash');const multer = require('multer');const upload = multer();const bcrypt = require('bcrypt');const saltRounds = 10;// POST /register - beginapp.post('/register', upload.none(), function(req, res) { // check to make sure none of the fields are empty if ( _.isEmpty(req.body.name) || _.isEmpty(req.body.email) || _.isEmpty(req.body.company_name) || _.isEmpty(req.body.password) ) { return res.json({ "status": false, "message": "All fields are required." }); } // any other intended checks// ...
Se comprueba si alguno de los campos está vacío y si los datos enviados coinciden con todas las especificaciones. Si se produce un error, se envía un mensaje de error al usuario como respuesta. En caso contrario, se codifica la contraseña y los datos se almacenan en la base de datos y se envía una respuesta al usuario informándole de que está registrado.
servidor.js
// ... bcrypt.hash(req.body.password, saltRounds, function(err, hash) { let db = new sqlite3.Database('./database/InvoicingApp.db'); let sql = `INSERT INTO users( name, email, company_name, password ) VALUES( '${req.body.name}', '${req.body.email}', '${req.body.company_name}', '${hash}' )`; db.run(sql, function(err) { if (err) { throw err; } else { return res.json({ "status": true, "message": "User Created." }); } }); db.close(); });});// POST /register - end
Ahora, si usamos una herramienta como Postman para enviar una solicitud POST a /register
con name
, email
, company_name
, y password
, creará un nuevo usuario:
Llave | Valor |
---|---|
name |
Usuario de prueba |
email |
example@example.com |
company_name |
Empresa de prueba |
password |
contraseña |
Podemos utilizar una consulta y mostrar la Users
tabla para verificar la creación del usuario:
- select * from users;
La base de datos ahora contiene un usuario recién creado:
Output1|Test User|example@example.com|Test Company|[hashed password]
Tu /register
ruta ahora está verificada.
CORREO/login
Si un usuario existente intenta iniciar sesión en el sistema mediante la /login
ruta, deberá proporcionar su dirección de correo electrónico y contraseña. Una vez que lo haga, la ruta gestionará la solicitud de la siguiente manera:
servidor.js
// ...// POST /login - beginapp.post('/login', upload.none(), function(req, res) { let db = new sqlite3.Database('./database/InvoicingApp.db'); let sql = `SELECT * from users where email='${req.body.email}'`; db.all(sql, [], (err, rows) = { if (err) { throw err; } db.close(); if (rows.length == 0) { return res.json({ "status": false, "message": "Sorry, wrong email." }); }// ...
Se realiza una consulta a la base de datos para obtener el registro del usuario con un correo electrónico determinado. Si el resultado devuelve una matriz vacía, significa que el usuario no existe y se envía una respuesta informando al usuario del error.
Si la consulta a la base de datos devuelve datos del usuario, se realiza una comprobación adicional para ver si la contraseña ingresada coincide con la contraseña registrada en la base de datos. Si es así, se envía una respuesta con los datos del usuario.
servidor.js
// ... let user = rows[0]; let authenticated = bcrypt.compareSync(req.body.password, user.password); delete user.password; if (authenticated) { return res.json({ "status": true, "user": user }); } return res.json({ "status": false, "message": "Wrong password. Please retry." }); });});// POST /login - end// ...
Cuando se prueba la ruta, recibirá un resultado exitoso o fallido.
Ahora, si usamos una herramienta como Postman para enviar una solicitud POST a /login
con email
y password
, enviará una respuesta.
Llave | Valor |
---|---|
email |
example@example.com |
password |
contraseña |
Como este usuario existe en la base de datos, obtenemos la siguiente respuesta:
Output{ "status": true, "user": { "id": 1, "name": "Test User", "email": "example@example.com", "company_name": "Test Company" }}
Tu /login
ruta ahora está verificada.
CORREO/invoice
La /invoice
ruta se encarga de la creación de una factura. Los datos que se pasan a la ruta incluyen el ID de usuario, el nombre de la factura y el estado de la misma. También incluye las transacciones singulares que componen la factura.
El servidor maneja la solicitud de la siguiente manera:
servidor.js
// ...// POST /invoice - beginapp.post('/invoice', upload.none(), function(req, res) { // validate data if (_.isEmpty(req.body.name)) { return res.json({ "status": false, "message": "Invoice needs a name." }); } // perform other checks// ...
En primer lugar, se validan los datos enviados al servidor. A continuación, se establece una conexión con la base de datos para las consultas posteriores.
servidor.js
// ... // create invoice let db = new sqlite3.Database('./database/InvoicingApp.db'); let sql = `INSERT INTO invoices( name, user_id, paid ) VALUES( '${req.body.name}', '${req.body.user_id}', 0 )`;// ...
INSERT
Se escribe y ejecuta la consulta necesaria para crear la factura. Posteriormente, se insertan las transacciones singulares en la transactions
tabla con la invoice_id
como clave externa para hacer referencia a ellas.
servidor.js
// ... db.serialize(function() { db.run(sql, function(err) { if (err) { throw err; } let invoice_id = this.lastID; for (let i = 0; i req.body.txn_names.length; i++) { let query = `INSERT INTO transactions( name, price, invoice_id ) VALUES( '${req.body.txn_names[i]}', '${req.body.txn_prices[i]}', '${invoice_id}' )`; db.run(query); } return res.json({ "status": true, "message": "Invoice created." }); }); });});// POST /invoice - end// ...
Ahora, si usamos una herramienta como Postman para enviar una solicitud POST a /invoice
con name
, user_id
, txn_names
, y txn_prices
, creará una nueva factura y registrará las transacciones:
Llave | Valor |
---|---|
name |
Factura de prueba |
user_id |
1 |
txn_names |
iPhone |
txn_prices |
600 |
txt_names |
MacBook |
txn_prices |
1700 |
A continuación, revise la tabla Facturas:
- select * from invoices;
Observe el siguiente resultado:
Output1|Test Invoice|1|0
Ejecute el siguiente comando:
- select * from transactions;
Observe el siguiente resultado:
Output1|iPhone|600|12|Macbook|1700|1
Tu /invoice
ruta ahora está verificada.
CONSEGUIR/invoice/user/{user_id}
Ahora, cuando un usuario quiere ver todas las facturas creadas, el cliente realizará una GET
solicitud a la /invoice/user/:id
ruta. user_id
Se pasa como parámetro de ruta. La solicitud se gestiona de la siguiente manera:
índice.js
// ...// GET /invoice/user/:user_id - beginapp.get('/invoice/user/:user_id', upload.none(), function(req, res) { let db = new sqlite3.Database('./database/InvoicingApp.db'); let sql = `SELECT * FROM invoices WHERE user_id='${req.params.user_id}' ORDER BY invoices.id`; db.all(sql, [], (err, rows) = { if (err) { throw err; } return res.json({ "status": true, "invoices": rows }); });});// GET /invoice/user/:user_id - end// ...
Se ejecuta una consulta para obtener todas las facturas y las transacciones relacionadas con la factura que pertenecen a un usuario en particular.
Considere una solicitud de todas las facturas de un usuario:
localhost:3128/invoice/user/1
Responderá con los siguientes datos:
Output{"status":true,"invoices":[{"id":1,"name":"Test Invoice","user_id":1,"paid":0}]}
Tu /invoice/user/:user_id
ruta ahora está verificada.
CONSEGUIR/invoice/user/{user_id}/{invoice_id}
Para obtener una factura específica, GET
se realiza una solicitud con user_id
y invoice_id
a la /invoice/user/{user_id}/{invoice_id}
ruta. La solicitud se gestiona de la siguiente manera:
índice.js
// ...// GET /invoice/user/:user_id/:invoice_id - beginapp.get('/invoice/user/:user_id/:invoice_id', upload.none(), function(req, res) { let db = new sqlite3.Database('./database/InvoicingApp.db'); let sql = `SELECT * FROM invoices LEFT JOIN transactions ON invoices.id=transactions.invoice_id WHERE user_id='${req.params.user_id}' AND invoice_id='${req.params.invoice_id}' ORDER BY transactions.id`; db.all(sql, [], (err, rows) = { if (err) { throw err; } return res.json({ "status": true, "transactions": rows }); });});// GET /invoice/user/:user_id/:invoice_id - end// set application port// ...
Se ejecuta una consulta para obtener una sola factura y las transacciones relacionadas con la factura que pertenece al usuario.
Considere una solicitud de una factura específica para un usuario:
localhost:3128/invoice/user/1/1
Responderá con los siguientes datos:
Output{"status":true,"transactions":[{"id":1,"name":"iPhone","user_id":1,"paid":0,"price":600,"invoice_id":1},{"id":2,"name":"Macbook","user_id":1,"paid":0,"price":1700,"invoice_id":1}]}
Tu /invoice/user/:user_id/:invoice_id
ruta ahora está verificada.
Conclusión
En este tutorial, configurará su servidor con todas las rutas necesarias para una aplicación de facturación liviana.
Continúe su aprendizaje con Cómo crear una aplicación de facturación liviana con Node: Interfaz de usuario .
Deja una respuesta