System overview
DAF Backend implements a dual-system architecture that operates two independent business domains:
POS System Database : Separate PostgreSQL database with credential-based connectionsPurpose : In-store retail operations including inventory, invoices, clients, suppliersPrefix : /api/pos/*
E-commerce System Database : Separate PostgreSQL database with pooled connectionsPurpose : Online shopping platform with cart, products, and paymentsPrefix : /api/ecom/*
Application entry points
The application has two main entry files:
Server initialization (src/server.js)
The server entry point loads environment variables and starts the Express server: require ( 'dotenv' ). config ();
const app = require ( './app' );
// Obtener puerto del .env o defecto
const PORT = process . env . PORT || 3000 ;
app . listen ( PORT , () => {
console . log ( `🚀 Servidor ejecutándose en http://localhost: ${ PORT } ` );
});
This file:
Loads .env configuration
Imports the Express app
Starts the HTTP server on the specified port
Application configuration (src/app.js)
The app configuration defines middleware, routes, and error handlers: const app = express ();
let corsConfiguration = {
origin: process . env . FRONTEND_IP ? [ process . env . FRONTEND_IP ] : [ 'http://localhost:5173' , 'http://localhost:5174' ],
methods: [ 'GET' , 'POST' , 'PUT' , 'DELETE' ],
allowedHeaders: [ 'Content-Type' , 'Authorization' ],
credentials: true
}
app . use ( cors ( corsConfiguration ));
app . use ( express . json ());
MVC architecture pattern
DAF Backend follows the Model-View-Controller (MVC) pattern, adapted for a REST API:
src/
├── routes/ → Define API endpoints and HTTP methods
├── controllers/ → Handle business logic and request/response
├── models/ → Database queries and data access
├── dtos/ → Data validation and transformation
├── middlewares/ → Authentication and file upload processing
└── config/ → Database connections and configuration
Routes layer
Routes define the API endpoints and map them to controller functions:
POS Routes
E-commerce Routes
// RUTAS POS
app . use ( '/api/pos/auth' , require ( './routes/pos.auth.routes' ));
app . use ( '/api/pos/cliente' , clienteRoutes );
app . use ( '/api/pos/unidadmedida' , unidadMedidaRoutes );
app . use ( '/api/pos/transaccion' , transaccionRoutes );
app . use ( '/api/pos/producto' , require ( './routes/pos.producto.routes' ));
app . use ( '/api/pos/estandar' , require ( './routes/pos.estandar.routes' ));
Example POS auth routes: src/routes/pos.auth.routes.js
const express = require ( 'express' );
const router = express . Router ();
const { login } = require ( '../controllers/pos.auth.controller.js' );
const { verifyToken } = require ( '../middlewares/pos.auth.middleware.js' );
const { getAccess } = require ( '../controllers/pos.access.controller.js' );
// POS Routes
router . post ( '/login' , login );
router . get ( '/access' , verifyToken , getAccess );
module . exports = router ;
// Rutas e-com
app . use ( '/api/ecom/auth' , appAuthRoutes );
app . use ( '/api/ecom/pagos' , pagosRoutes );
Example e-commerce auth routes: src/routes/ecom.auth.routes.js
const { Router } = require ( "express" );
const { AuthController } = require ( "../controllers/ecom.auth.controller.js" );
const { verifyToken } = require ( "../middlewares/pos.auth.middleware.js" );
const router = Router ();
router . post ( "/register" , AuthController . register );
router . post ( "/login" , AuthController . login );
router . get ( "/client/:cli_ruc_ced" , AuthController . clientAvailable );
// 🔐 Rutas protegidas
router . get ( "/me" , verifyToken , AuthController . profile );
router . put ( "/password" , verifyToken , AuthController . updatePassword );
router . delete ( "/" , verifyToken , AuthController . delete );
module . exports = router ;
Controllers layer
Controllers handle request processing, validation, and response formatting:
POS Controller Pattern (src/controllers/pos.cliente.controller.js:15-38)
E-commerce Controller Pattern (src/controllers/ecom.carrito.controller.js:12-37)
const create = async ( req , res ) => {
// 1. Validar datos de entrada
const errors = validateClienteDTO ( req . body );
if ( errors . length ) return res . status ( 400 ). json ({ errors });
const pool = connectFromJWT ( req );
try {
const result = await modeloCliente . createCliente ( pool , {
... req . body
});
res . status ( 201 ). json ({
message: "Cliente creado exitosamente" ,
data: result
});
} catch ( error ) {
console . error ( "Error en createCliente:" , error . message );
res . status ( 500 ). json ({ message: "Error interno al crear cliente" , detail: error . message });
} finally {
await pool . end ();
}
};
Key Difference : POS controllers create and close database pools per request, while e-commerce controllers use a shared connection pool.
Models layer
Models encapsulate database queries and data access logic:
src/models/cliente.model.js:43-55
const getAllClientes = async ( pool , page = 1 ) => {
const limit = 20 ;
const offset = ( page - 1 ) * limit ;
const query = `
SELECT * FROM public.cliente WHERE cli_estado = ' ${ process . env . ACTIVE_STATUS_INDEPENDENT } '
ORDER BY cli_nombre ASC
LIMIT $1 OFFSET $2
` ;
const result = await pool . query ( query , [ limit , offset ]);
return result . rows ;
};
DTOs (Data Transfer Objects)
DTOs validate and transform incoming request data:
Custom Validation
Zod Validation
src/dtos/cliente.dto.js:3-43
const validateClienteDTO = ( data , isUpdate = false ) => {
const errors = [];
const validStatuses = [ 'ACT' , 'INA' , 'SUS' ];
// Validación de Ciudad (FK)
if ( ! isUpdate || data . ct_codigo !== undefined ) {
if ( ! data . ct_codigo || data . ct_codigo . length > 10 ) {
errors . push ( 'ct_codigo es requerido y máximo 10 caracteres' );
}
}
// Nombre
if ( ! isUpdate || data . cli_nombre !== undefined ) {
if ( ! data . cli_nombre || data . cli_nombre . length > 120 ) {
errors . push ( 'cli_nombre es requerido y máximo 120 caracteres' );
}
}
// RUC / Cédula (13 dígitos)
if ( ! isUpdate || data . cli_ruc_ced !== undefined ) {
if ( ! data . cli_ruc_ced || ! / ^ \d / . test ( data . cli_ruc_ced )) {
errors . push ( 'cli_ruc_ced es requerido y debe ser numérico' );
}
}
// Teléfono (10 dígitos)
if ( ! isUpdate || data . cli_telefono !== undefined ) {
if ( ! data . cli_telefono || ! / ^ \d {10} $ / . test ( data . cli_telefono )) {
errors . push ( 'cli_telefono es requerido y debe tener 10 dígitos numéricos' );
}
}
// Email
if ( ! isUpdate || data . cli_mail !== undefined ) {
if ( ! data . cli_mail || data . cli_mail . length > 60 ) {
errors . push ( 'cli_mail es requerido y máximo 60 caracteres' );
}
}
return errors ;
};
const { z } = require ( "zod" );
const registerSchema = z . object ({
email: z . string (). email (). max ( 60 ),
password: z . string (). min ( 8 ). max ( 60 ),
cli_ruc_ced: z . string (). min ( 10 ). max ( 13 ),
cliente: z . object ({
cli_nombre: z . string (). max ( 120 ),
cli_telefono: z . string (). max ( 10 ),
cli_celular: z . string (). max ( 9 ),
cli_direccion: z . string (). max ( 60 ),
ct_codigo: z . string (). max ( 10 )
}). optional ()
});
const loginSchema = z . object ({
email: z . string (). email (),
password: z . string ()
});
Middleware pipeline
Requests flow through several middleware layers:
Global middleware
CORS (Cross-Origin Resource Sharing)
Configured to allow requests from specified frontend origins: let corsConfiguration = {
origin: process . env . FRONTEND_IP ? [ process . env . FRONTEND_IP ] : [ 'http://localhost:5173' , 'http://localhost:5174' ],
methods: [ 'GET' , 'POST' , 'PUT' , 'DELETE' ],
allowedHeaders: [ 'Content-Type' , 'Authorization' ],
credentials: true
}
app . use ( cors ( corsConfiguration ));
JSON body parser
Parses incoming JSON request bodies:
Static file serving
Serves uploaded product images: app . use ( '/images' , express . static ( path . join ( __dirname , 'images' )));
Route-specific middleware
Authentication
File Upload
JWT token verification middleware protects routes: src/middlewares/pos.auth.middleware.js:4-33
const verifyToken = ( req , res , next ) => {
const authHeader = req . headers [ 'authorization' ];
if ( ! authHeader ) {
return res . status ( 401 ). json ({
message: 'Token no proporcionado' ,
});
}
const token = authHeader . split ( ' ' )[ 1 ]; // Authorization: Bearer {TOKEN}
if ( ! token ) {
return res . status ( 401 ). json ({
message: 'Token inválido' ,
});
}
try {
const decoded = jwt . verify ( token , process . env . JWT_SECRET );
// 🔒 Se mantiene compatibilidad con todo lo existente
req . user = decoded ;
next ();
} catch ( error ) {
return res . status ( 401 ). json ({
message: 'Token inválido o expirado' ,
});
}
};
Multer middleware handles product image uploads: src/middlewares/upload.js
const multer = require ( 'multer' );
const path = require ( 'path' );
const fs = require ( 'fs' );
const imagesDir = path . join ( __dirname , '../images' );
if ( ! fs . existsSync ( imagesDir )) {
fs . mkdirSync ( imagesDir );
}
const storage = multer . diskStorage ({
destination : ( req , file , cb ) => {
cb ( null , imagesDir );
},
filename : ( req , file , cb ) => {
const ext = path . extname ( file . originalname );
const filename = `producto_ ${ Date . now () }${ ext } ` ;
cb ( null , filename );
}
});
const fileFilter = ( req , file , cb ) => {
if ( ! file . mimetype . startsWith ( 'image/' )) {
cb ( new Error ( 'Solo se permiten imágenes' ), false );
}
cb ( null , true );
};
const upload = multer ({
storage ,
fileFilter ,
limits: { fileSize: 5 * 1024 * 1024 }
});
Features :
Saves images to src/images directory
Generates unique filenames with timestamp
Validates file type (images only)
Limits file size to 5MB
Database connection strategy
The two systems use different connection strategies:
POS (Credential-Based)
E-commerce (Pooled)
POS connection pooling Each POS request creates a new connection pool with user credentials from JWT: const { Pool } = require ( 'pg' );
require ( 'dotenv' ). config ();
function getConnectionWithCredentials ( user , password ) {
return new Pool ({
host: process . env . POS_HOST ,
port: process . env . POS_PORT ,
database: process . env . POS_NAME ,
user ,
password ,
});
}
module . exports = {
getConnectionWithCredentials ,
};
Usage in controllers :src/controllers/pos.cliente.controller.js:6-9
const connectFromJWT = ( req ) => {
const { usuario , password } = req . user ;
return getConnectionWithCredentials ( usuario , password );
};
Benefits :
Database-level security and authentication
Audit trails show actual user performing operations
Fine-grained permission control via PostgreSQL roles
Trade-offs :
Higher connection overhead (create and close pool per request)
Must manage pool cleanup in finally blocks
E-commerce connection pooling E-commerce uses a single shared connection pool: const { Pool } = require ( 'pg' );
require ( 'dotenv' ). config ();
const pool = new Pool ({
host: process . env . EC_HOST ,
port: process . env . EC_PORT ,
database: process . env . EC_NAME ,
user: process . env . EC_USER ,
password: process . env . EC_PASSWORD ,
});
const getConnection = () => {
return pool ;
};
module . exports = {
getConnection ,
};
Benefits :
Lower connection overhead (reuse existing connections)
Better performance for high-traffic scenarios
Simpler controller code
Trade-offs :
All database operations appear to come from same user
Application-level authorization required
No database-level user tracking
Error handling flow
The API implements a 404 catch-all handler:
app . use (( req , res ) => {
res . status ( 404 ). json ({ message: 'Not Found' });
});
Controller-level error handling follows a try-catch-finally pattern:
POS Error Handling
E-commerce Error Handling
const getAll = async ( req , res ) => {
const pool = connectFromJWT ( req );
try {
const page = parseInt ( req . query . page ) || 1 ;
const result = await modeloCliente . getAllClientes ( pool , page );
res . status ( 200 ). json ({
message: "Listado obtenido correctamente" ,
count: result . length ,
data: result
});
} catch ( error ) {
res . status ( 500 ). json ({ message: error . message });
} finally {
await pool . end (); // Always close the pool
}
};
Request lifecycle
Here’s a complete request lifecycle for a POS endpoint:
Client sends request
curl -X GET http://localhost:3000/api/pos/cliente \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
CORS middleware
Validates origin and adds CORS headers
Router matches route
Express router matches /api/pos/cliente to cliente routes
verifyToken middleware
Extracts and validates JWT token, decodes payload to req.user
Controller executes
Extracts credentials from req.user
Creates database pool with credentials
Calls model function to query database
Model queries database
Executes SQL query and returns results
Controller formats response
Formats data and sends JSON response with appropriate status code
Pool cleanup
finally block closes the database pool
Directory structure reference
daf-backend/
├── src/
│ ├── app.js → Express app configuration
│ ├── server.js → Server entry point
│ ├── config/
│ │ ├── db_pos.js → POS credential-based connections
│ │ ├── db_ecom.js → E-commerce pooled connection
│ │ ├── db.factura.js → Invoice-specific config
│ │ └── db.proveedor.js → Supplier-specific config
│ ├── routes/
│ │ ├── pos.*.routes.js → POS endpoint definitions
│ │ └── ecom.*.routes.js → E-commerce endpoint definitions
│ ├── controllers/
│ │ ├── pos.*.controller.js → POS business logic
│ │ └── ecom.*.controller.js → E-commerce business logic
│ ├── models/
│ │ └── *.model.js → Database query functions
│ ├── dtos/
│ │ └── *.dto.js → Validation schemas
│ ├── middlewares/
│ │ ├── pos.auth.middleware.js → JWT verification
│ │ ├── auth.middleware.js → Simplified JWT verification
│ │ └── upload.js → File upload handling
│ └── images/ → Uploaded product images
├── package.json
├── .env → Environment configuration
└── node_modules/
Next steps
Database Deep dive into database configuration and schema
Authentication Learn about JWT tokens and credential-based connections
Error Handling Understand error responses and debugging
POS API Reference Explore all POS endpoints