Node.js is a JavaScript runtime built on Chrome's V8 engine that lets you run JavaScript outside the browser — on servers, CLI tools, scripts, and beyond. It was created by Ryan Dahl in 2009 to solve the problem of concurrent connections in web servers.
Google's open-source JavaScript engine that compiles JS to native machine code. Node.js embeds V8 and adds APIs for file system, networking, and OS access.
REST APIs · Real-time apps (WebSockets) · Microservices · CLI tools · Serverless functions · Backend for React/Vue/Next.js
Non-blocking I/O · High concurrency · Single language full-stack · Massive npm ecosystem · Fast startup time
Not ideal for CPU-intensive tasks (use worker threads) · Callback hell (solved by async/await) · Single-threaded by default
Node.js uses a single-threaded event loop combined with libuv's thread pool to handle thousands of concurrent connections without blocking. When you make a file read or DB query, Node delegates it and moves on — executing the callback when the operation finishes.
async/await
Microtasks
Macrotasks
OS async I/O
Network · DNS
# Install Node.js v20 LTS via nvm (recommended) curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash nvm install 20 nvm use 20 # Verify installation node --version # v20.x.x npm --version # 10.x.x # Run a JavaScript file directly node index.js # Start a REPL (interactive shell) node
// ── COMMONJS: require / module.exports ────────────────── // math/operations.jsconst add = (a, b) => a + b;const subtract = (a, b) => a - b;const multiply = (a, b) => a * b; // Named exports object module.exports = { add, subtract, multiply }; // ── OR: single export ─────────────────────────────────── module.exports =class Calculator {add (a, b) {return a + b; }subtract (a, b) {return a - b; } }; // ── Importing in another file ───────────────────────────const { add, multiply } =require ('./math/operations' );const path =require ('path' ); // built-inconst express =require ('express' ); // npm package console.log (add(3 ,4 )); // 7 // ── ES MODULES: import / export (add "type":"module" to package.json) ── // mathUtils.mjsexport const add = (a, b) => a + b;export default function divide (a, b) {if (b ===0 )throw new Error('Division by zero' );return a / b; } // importing ESMimport divide, { add }from './mathUtils.mjs' ;import *as utilsfrom './mathUtils.mjs' ;
const path =require ('path' );const os =require ('os' ); // ── PATH ────────────────────────────────────────────────const fullPath = path.join (__dirname,'data' ,'users.json' ); console.log (path.extname ('server.js' )); // .js console.log (path.basename ('/api/users' )); // users console.log (path.dirname ('/api/users/1' )); // /api/users // ── OS ────────────────────────────────────────────────── console.log (os.platform ()); // linux / darwin / win32 console.log (os.cpus ().length ); // number of CPU cores console.log (os.freemem () /1024 **3 ,'GB free' ); // ── HTTP (raw server) ───────────────────────────────────const http =require ('http' );const server = http.createServer ((req, res) => { res.writeHead (200 , {'Content-Type' :'application/json' }); res.end (JSON.stringify ({ status:'ok' })); }); server.listen (3000 );
const fs =require ('fs' );const fsP =require ('fs/promises' ); // promise-based API (recommended)const path =require ('path' ); // ── ASYNC READ (promise-based) ──────────────────────────async function readConfig () {try {const filePath = path.join (__dirname,'config.json' );const content =await fsP.readFile (filePath,'utf-8' );return JSON.parse (content); }catch (err) {if (err.code ==='ENOENT' )throw new Error('Config file not found' );throw err; } } // ── WRITE FILE ──────────────────────────────────────────async function saveData (filename, data) {await fsP.writeFile (filename, JSON.stringify (data,null ,2 ),'utf-8' ); console.log (`Saved to ${filename}` ); } // ── APPEND, DELETE, CHECK EXISTS ───────────────────────await fsP.appendFile ('logs/app.log' ,`[${new Date().toISOString()}] started\n` );await fsP.unlink ('temp.txt' ); // deleteconst exists = fs.existsSync ('config.json' ); // sync check // ── READ DIRECTORY ──────────────────────────────────────const files =await fsP.readdir ('./' ); files.forEach (f => console.log (f));
# Initialise a new project npm init -y # Install production dependency npm install express # saves to "dependencies" # Install development dependency npm install --save-dev nodemon jest # Install globally (CLI tools) npm install -g pm2 # Lock exact version npm install express@4.18.2 # Remove package npm uninstall express # See installed packages npm list --depth=0
{
"name" : "vendorflow-api" ,
"version" : "1.0.0" ,
"description" : "REST API backend" ,
"main" : "src/server.js" ,
"scripts" : {
"start" : "node src/server.js" ,
"dev" : "nodemon src/server.js" , // auto-restart on changes
"test" : "jest --coverage" ,
"lint" : "eslint src/**/*.js" ,
"migrate" : "node src/db/migrate.js" ,
"seed" : "node src/db/seed.js"
},
"dependencies" : {
"express" : "^4.18.2" ,
"bcryptjs" : "^2.4.3" ,
"jsonwebtoken" : "^9.0.0" ,
"sequelize" : "^6.35.0" ,
"mysql2" : "^3.6.0" ,
"dotenv" : "^16.3.1" ,
"joi" : "^17.11.0"
},
"devDependencies" : {
"nodemon" : "^3.0.2" ,
"jest" : "^29.7.0" ,
"supertest" : "^6.3.3"
}
}
vendorflow-api/ ├── src/ │ ├── config/ │ │ ├── database.js # DB connection config │ │ └── app.js # Express app setup │ ├── controllers/ # Request handlers (thin layer) │ │ ├── authController.js │ │ ├── userController.js │ │ └── paymentController.js │ ├── services/ # Business logic │ │ ├── authService.js │ │ └── paymentService.js │ ├── models/ # Sequelize / Mongoose models │ │ ├── User.js │ │ └── Payment.js │ ├── routes/ # Route definitions │ │ ├── auth.routes.js │ │ ├── user.routes.js │ │ └── index.js # Combine all routes │ ├── middlewares/ │ │ ├── auth.middleware.js # JWT verification │ │ ├── validate.js # Joi validation │ │ └── errorHandler.js │ ├── utils/ │ │ ├── ApiError.js │ │ ├── logger.js │ │ └── helpers.js │ └── server.js # Entry point ├── tests/ │ ├── unit/ │ └── integration/ ├── .env ├── .env.example ├── .gitignore └── package.json
const http =require ('http' );const PORT = process.env.PORT ||3000 ;const server = http.createServer (async (req, res) => {const { method, url } = req; // Parse JSON body from incoming requestconst parseBody = () =>new Promise((resolve) => {let body ='' ; req.on ('data' , chunk => body += chunk.toString ()); req.on ('end' , () => resolve(body ? JSON.parse (body) : {})); }); // Helper — send JSON responseconst json = (statusCode, data) => { res.writeHead (statusCode, {'Content-Type' :'application/json' }); res.end (JSON.stringify (data)); }; // Routingif (method ==='GET' && url ==='/' )return json (200 , { message:'Node.js API running!' });if (method ==='POST' && url ==='/echo' ) {const body =await parseBody ();return json (200 , { received: body }); }json (404 , { error:'Route not found' }); }); server.listen (PORT, () => console.log (`Server running on http://localhost:${PORT}` ) );
const express =require ('express' );const cors =require ('cors' );const helmet =require ('helmet' ); // security headersconst morgan =require ('morgan' ); // request loggingconst routes =require ('../routes' );const app =express (); // ── GLOBAL MIDDLEWARE ─────────────────────────────────── app.use (helmet ()); // Set security HTTP headers app.use (cors ({ origin: process.env.CLIENT_URL ||'*' , credentials:true , })); app.use (morgan ('combined' )); // Log all requests app.use (express.json ({ limit:'10mb' })); // Parse JSON body app.use (express.urlencoded ({ extended:true })); // Parse form data // ── ROUTES ────────────────────────────────────────────── app.use ('/api/v1' , routes); // ── HEALTH CHECK ──────────────────────────────────────── app.get ('/health' , (req, res) => { res.json ({ status:'healthy' , timestamp:new Date() }); }); // ── 404 HANDLER ───────────────────────────────────────── app.use ('*' , (req, res) => { res.status (404 ).json ({ success:false , message:'Route not found' }); }); module.exports = app;
const jwt =require ('jsonwebtoken' ); // Middleware: a function with (req, res, next) signature // Call next() to pass control to the next middleware/routeconst authenticate = (req, res, next) => {try {const authHeader = req.headers.authorization ;if (!authHeader?.startsWith('Bearer ' )) {return res.status (401 ).json ({ success:false , message:'No token provided' }); }const token = authHeader.split (' ' )[1 ];const decoded = jwt.verify (token, process.env.JWT_SECRET); req.user = decoded; // attach user to requestnext (); // continue to route handler }catch (error) { res.status (401 ).json ({ success:false , message:'Invalid or expired token' }); } }; // Role-based access — factory middlewareconst authorize = (...roles) => (req, res, next) => {if (!roles.includes (req.user?.role)) {return res.status (403 ).json ({ success:false , message:'Insufficient permissions' }); }next (); }; // Request logger middlewareconst requestLogger = (req, res, next) => { console.log (`[${new Date().toISOString()}] ${req.method} ${req.path}` );next (); }; module.exports = { authenticate, authorize, requestLogger };
const express =require ('express' );const router = express.Router ();const controller =require ('../controllers/userController' );const { authenticate, authorize } =require ('../middlewares/auth.middleware' );const validate =require ('../middlewares/validate' );const { updateUserSchema } =require ('../validators/user.validator' ); // GET /api/v1/users?page=1&limit=10&role=admin router.get ('/' , authenticate, controller.getAll ); // GET /api/v1/users/:id router.get ('/:id' , authenticate, controller.getById ); // PUT /api/v1/users/:id — with validation middleware router.put ('/:id' , authenticate,validate (updateUserSchema), controller.update ); // DELETE /api/v1/users/:id — admin only router.delete ('/:id' , authenticate,authorize ('admin' ), controller.delete ); module.exports = router;
| Method | Path | Action | Status |
|---|---|---|---|
| GET | /api/v1/users | List all users (with pagination) | 200 |
| GET | /api/v1/users/:id | Get single user | 200 / 404 |
| POST | /api/v1/users | Create new user | 201 |
| PUT | /api/v1/users/:id | Replace user (full update) | 200 / 404 |
| PATCH | /api/v1/users/:id | Partial update | 200 / 404 |
| DELETE | /api/v1/users/:id | Delete user | 204 / 404 |
const userService =require ('../services/userService' );const { ApiError } =require ('../utils/ApiError' ); // Thin controller — delegates to service layerconst getAll =async (req, res, next) => {try {const { page =1 , limit =10 , role, search } = req.query;const result =await userService.findAll ({ page: parseInt(page), limit: parseInt(limit), role, search }); res.json ({ success:true , data: result }); }catch (err) {next (err); } };const getById =async (req, res, next) => {try {const user =await userService.findById (req.params.id );if (!user)throw new ApiError(404 ,'User not found' ); res.json ({ success:true , data: user }); }catch (err) {next (err); } };const update =async (req, res, next) => {try {const updated =await userService.update (req.params.id , req.body);if (!updated)throw new ApiError(404 ,'User not found' ); res.json ({ success:true , data: updated, message:'User updated' }); }catch (err) {next (err); } };const remove =async (req, res, next) => {try {await userService.delete (req.params.id ); res.status (204 ).send (); }catch (err) {next (err); } }; module.exports = { getAll, getById, update, remove };
// ── DATABASE CONNECTION ──────────────────────────────────const { Sequelize } =require ('sequelize' );require ('dotenv' ).config ();const sequelize =new Sequelize( process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, { host: process.env.DB_HOST, dialect:'mysql' , pool: { max:10 , min:0 , acquire:30000 , idle:10000 }, logging: process.env.NODE_ENV ==='development' ? console.log :false , } ); module.exports = sequelize; // ── USER MODEL ──────────────────────────────────────────const { DataTypes, Model } =require ('sequelize' );const sequelize =require ('../config/database' );class Userextends Model {} User.init ({ id: { type: DataTypes.UUID , defaultValue: DataTypes.UUIDV4 , primaryKey:true }, name: { type: DataTypes.STRING (100 ), allowNull:false }, email: { type: DataTypes.STRING , allowNull:false , unique:true }, password: { type: DataTypes.STRING , allowNull:false }, role: { type: DataTypes.ENUM ('user' ,'admin' ), defaultValue:'user' }, active: { type: DataTypes.BOOLEAN , defaultValue:true }, }, { sequelize, modelName:'User' , tableName:'users' , timestamps:true , // adds createdAt, updatedAt paranoid:true , // soft delete (adds deletedAt) hooks: {beforeCreate :async (user) => { user.email = user.email.toLowerCase (); }, }, }); module.exports = User;
const mongoose =require ('mongoose' );const productSchema =new mongoose.Schema ({ name: { type: String, required: [true ,'Product name is required' ], trim:true }, description: { type: String, maxLength:1000 }, price: { type: Number, required:true , min:0 }, category: { type: String, enum: ['electronics' ,'clothing' ,'food' ] }, stock: { type: Number, default:0 , min:0 }, images: [{ type: String }], seller: { type: mongoose.Schema.Types.ObjectId, ref:'User' , required:true }, tags: [String], active: { type: Boolean, default:true }, }, { timestamps:true , // auto createdAt & updatedAt toJSON: { virtuals:true }, }); // Virtual — computed field, not stored in DB productSchema.virtual('isInStock' ).get (function () {return this .stock >0 ; }); // Index for fast search productSchema.index ({ name:'text' , description:'text' }); productSchema.index ({ seller:1 , category:1 }); module.exports = mongoose.model ('Product' , productSchema);
const { Op } =require ('sequelize' );const User =require ('../models/User' );const { ApiError } =require ('../utils/ApiError' );const findAll =async ({ page, limit, role, search }) => {const where = {};if (role) where.role = role;if (search) where.name = { [Op.iLike ]:`%${search}%` };const { count, rows } =await User.findAndCountAll ({ where, attributes: { exclude: ['password' ] }, // never expose password limit, offset: (page -1 ) * limit, order: [['createdAt' ,'DESC' ]], });return { users: rows, total: count, pages: Math.ceil (count / limit), currentPage: page, }; };const findById =async (id) => {return User.findByPk (id, { attributes: { exclude: ['password' ] } }); };const update =async (id, data) => {const user =await User.findByPk (id);if (!user)return null ;return user.update (data); };const deleteUser =async (id) => {const user =await User.findByPk (id);if (!user)throw new ApiError(404 ,'User not found' );await user.destroy (); // soft delete if paranoid:true }; module.exports = { findAll, findById, update, delete: deleteUser };
const bcrypt =require ('bcryptjs' );const jwt =require ('jsonwebtoken' );const User =require ('../models/User' );const { ApiError } =require ('../utils/ApiError' );const register =async ({ name, email, password }) => { // 1. Check if email already existsconst existing =await User.findOne ({ where: { email } });if (existing)throw new ApiError(409 ,'Email already registered' ); // 2. Hash password — saltRounds 12 is the production standardconst hashedPassword =await bcrypt.hash (password,12 ); // 3. Create userconst user =await User.create ({ name, email, password: hashedPassword, }); // 4. Return user without passwordconst { password: _, ...safeUser } = user.toJSON ();return { user: safeUser, token:generateToken (user) }; };const login =async ({ email, password }) => { // Find user — include password for comparisonconst user =await User.findOne ({ where: { email } }); // Generic error — don't reveal which field was wrongif (!user)throw new ApiError(401 ,'Invalid credentials' );const isValid =await bcrypt.compare (password, user.password);if (!isValid)throw new ApiError(401 ,'Invalid credentials' );if (!user.active)throw new ApiError(403 ,'Account suspended' );const { password: _, ...safeUser } = user.toJSON ();return { user: safeUser, token:generateToken (user) }; };const generateToken = (user) => jwt.sign ( { id: user.id, role: user.role, email: user.email }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES ||'7d' , algorithm:'HS256' } ); module.exports = { register, login };
email + password
Sign JWT
expires 7d
→ verify → allow
iss and aud claims in production.
// ── 1. PARAMETERISED QUERIES (prevents SQL injection) ─── // ❌ NEVER do this:const sql =`SELECT * FROM users WHERE email = '${email}'` ; // ✅ ALWAYS use Sequelize (parameterised automatically) or: User.findOne ({ where: { email } }); // Sequelize db.query ('SELECT * WHERE email = ?' , [email]); // Raw SQL // ── 2. INPUT VALIDATION WITH JOI ────────────────────────const Joi =require ('joi' );const registerSchema = Joi.object ({ name: Joi.string().min (2 ).max (50 ).required(), email: Joi.string().min (8 ).pattern (/^(?=.*[A-Z])(?=.*\d)/).required (), }); // ── 3. RATE LIMITING ────────────────────────────────────const rateLimit =require ('express-rate-limit' );const loginLimiter =rateLimit ({ windowMs:15 *60 *1000 , // 15 minutes max:5 , // 5 attempts per window message:'Too many login attempts' , standardHeaders:true , }); app.use ('/api/auth/login' , loginLimiter); // ── 4. HELMET (HTTP security headers) ─────────────────── app.use (helmet ()); // Sets: X-Content-Type-Options, X-Frame-Options, // Content-Security-Policy, HSTS, and more // ── 5. SANITIZE INPUT (prevent XSS) ─────────────────────const xss =require ('xss-clean' ); app.use (xss ()); // strips HTML tags from req.body/query/params
// Centralised error class — extends native Errorclass ApiErrorextends Error {constructor (statusCode, message, errors = [], stack ='' ) {super (message);this .statusCode = statusCode;this .success =false ;this .errors = errors;if (stack) {this .stack = stack; }else { Error.captureStackTrace (this ,this .constructor); } } // Factory methods for common errorsstatic notFound (msg ='Resource not found' ) {return new ApiError(404 , msg); }static unauthorized (msg ='Unauthorized' ) {return new ApiError(401 , msg); }static forbidden (msg ='Forbidden' ) {return new ApiError(403 , msg); }static badRequest (msg ='Bad request' ) {return new ApiError(400 , msg); }static conflict (msg ='Resource already exists' ) {return new ApiError(409 , msg); } } module.exports = { ApiError };
const { ApiError } =require ('../utils/ApiError' );const logger =require ('../utils/logger' ); // Express error middleware — MUST have 4 parametersconst errorHandler = (err, req, res, next) => {let error = { ...err }; error.message = err.message; // Log all errors in developmentif (process.env.NODE_ENV ==='development' ) { logger.error (`${err.statusCode || 500} - ${err.message} - ${req.originalUrl}`); } // Sequelize validation errorif (err.name ==='SequelizeValidationError' ) {const message = err.errors.map (e => e.message).join (', ' ); error =new ApiError(400 , message); } // Sequelize unique constraintif (err.name ==='SequelizeUniqueConstraintError' ) { error =new ApiError(409 ,'Duplicate field value' ); } // JWT errorsif (err.name ==='JsonWebTokenError' ) error =new ApiError(401 ,'Invalid token' );if (err.name ==='TokenExpiredError' ) error =new ApiError(401 ,'Token expired' ); res.status (error.statusCode ||500 ).json ({ success:false , message: error.message ||'Internal Server Error' , errors: error.errors || [], ...(process.env.NODE_ENV ==='development' && { stack: err.stack }), }); }; module.exports = errorHandler; // Register AFTER all routes in app.js: // app.use(errorHandler);
// ── 1. CALLBACKS (legacy — avoid in new code) ─────────── fs.readFile ('file.txt' ,'utf-8' , (err, data) => {if (err)return console.error (err); console.log (data); }); // ── 2. PROMISES ────────────────────────────────────────── fsP.readFile ('file.txt' ,'utf-8' ) .then (data => console.log (data)) .catch (err => console.error (err)) .finally (() => console.log ('Done' )); // ── 3. ASYNC/AWAIT (recommended) ────────────────────────async function processFile (filename) {try {const content =await fsP.readFile (filename,'utf-8' );const parsed = JSON.parse (content);return parsed; }catch (err) {throw new Error(`Failed to process ${filename}: ${err.message}` ); } } // ── PARALLEL EXECUTION ───────────────────────────────────async function fetchAllData (userId) { // Promise.all — runs concurrently, fails fast if any rejectsconst [user, orders, profile] =await Promise.all ([ User.findByPk (userId), Order.findAll ({ where: { userId } }), Profile.findOne ({ where: { userId } }), ]);return { user, orders, profile }; } // Promise.allSettled — collect all results, don't fail fastconst results =await Promise.allSettled ([fetchUser (),fetchOrders (),fetchInventory () ]); results.forEach (r => {if (r.status ==='fulfilled' ) console.log (r.value);else console.error (r.reason); });
const fs =require ('fs' );const csv =require ('csv-parser' );const { pipeline } =require ('stream/promises' );const { Transform } =require ('stream' ); // ── PIPE: read → transform → write ─────────────────────const processLargeFile =async (inputPath, outputPath) => {const reader = fs.createReadStream (inputPath);const writer = fs.createWriteStream (outputPath);const upperCase =new Transform({ transform(chunk, encoding, callback) {this .push (chunk.toString ().toUpperCase ());callback (); } }); // pipeline handles backpressure and cleanupawait pipeline (reader, upperCase, writer); console.log ('File processed successfully' ); }; // ── STREAM CSV FILE ─────────────────────────────────────const importUsersFromCSV = (filePath) =>new Promise((resolve, reject) => {const users = []; fs.createReadStream (filePath) .pipe (csv ()) .on ('data' , row => users.push ({ name: row.name, email: row.email })) .on ('end' , () =>resolve (users)) .on ('error' , reject); }); // ── STREAM LARGE API RESPONSE ─────────────────────────── app.get ('/export/users' , authenticate,async (req, res) => { res.setHeader ('Content-Type' ,'application/json' ); res.write ('[' );let first =true ;const cursor = User.findAll ({ raw:true , attributes: ['id' ,'name' ,'email' ] });for await (const userof cursor) {if (!first) res.write (',' ); res.write (JSON.stringify (user)); first =false ; } res.end (']' ); });
# .env.example — commit this, NOT .env!
NODE_ENV=development
PORT=3000
DB_HOST=localhost
DB_PORT=3306
DB_NAME=vendorflow_db
DB_USER=root
DB_PASSWORD=
JWT_SECRET=change-this-to-a-64-char-random-string
JWT_EXPIRES=7d
REDIS_URL=redis://localhost:6379
CLIENT_URL=http://localhost:5173
require ('dotenv' ).config (); // Validate all required env vars at startupconst required = ['DB_NAME' ,'DB_USER' ,'DB_PASSWORD' ,'JWT_SECRET' ,'NODE_ENV' ]; required.forEach (key => {if (!process.env[key]) { console.error (`❌ Missing required env var: ${key}` ); process.exit (1 ); // crash early, loudly } }); module.exports = { env: process.env.NODE_ENV ||'development' , port: parseInt(process.env.PORT) ||3000 , jwtSecret: process.env.JWT_SECRET, jwtExpires: process.env.JWT_EXPIRES ||'7d' , db: { host: process.env.DB_HOST, name: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, }, };
const bcrypt =require ('bcryptjs' );const authService =require ('../../src/services/authService' );const User =require ('../../src/models/User' ); // Mock the database model jest.mock ('../../src/models/User' );describe ('AuthService' , () => {beforeEach (() => jest.clearAllMocks ());describe ('register' , () => {it ('should register a new user successfully' ,async () => { User.findOne.mockResolvedValue (null ); // no existing user User.create.mockResolvedValue ({ id:'uuid-123' , name:'Alice' , email:'alice@test.com' , toJSON: () => ({ id:'uuid-123' , name:'Alice' }) });const result =await authService.register ({ name:'Alice' , email:'alice@test.com' , password:'SecurePass1!' });expect (result).toHaveProperty ('token' );expect (result.user).not .toHaveProperty ('password' ); });it ('should throw 409 if email already exists' ,async () => { User.findOne.mockResolvedValue ({ id:'existing' });await expect (authService.register ({ name:'Bob' , email:'taken@test.com' , password:'Pass123!' })).rejects .toMatchObject ({ statusCode:409 }); }); }); });
const request =require ('supertest' );const app =require ('../../src/config/app' );const sequelize =require ('../../src/config/database' );beforeAll (async () => {await sequelize.sync ({ force:true }); // reset test DB });afterAll (async () => {await sequelize.close (); });describe ('POST /api/v1/auth/register' , () => {it ('should create a new user and return token' ,async () => {const res =await request (app) .post ('/api/v1/auth/register' ) .send ({ name:'Alice' , email:'alice@test.com' , password:'SecurePass1!' }) .expect (201 );expect (res.body.success).toBe (true );expect (res.body.data).toHaveProperty ('token' );expect (res.body.data.user).not .toHaveProperty ('password' ); });it ('should reject invalid email' ,async () => {const res =await request (app) .post ('/api/v1/auth/register' ) .send ({ name:'Bob' , email:'not-an-email' , password:'pass' }) .expect (400 );expect (res.body.success).toBe (false ); }); });
const redis =require ('ioredis' );const client =new redis(process.env.REDIS_URL);const cache = {async get (key) {const data =await client.get (key);return data ? JSON.parse (data) :null ; },async set (key, value, ttlSeconds =300 ) {await client.setex (key, ttlSeconds, JSON.stringify (value)); },async del (key) {await client.del (key); },async invalidate (pattern) {const keys =await client.keys (pattern);if (keys.length)await client.del (...keys); }, }; // Cache middleware factoryconst cacheMiddleware = (keyFn, ttl =300 ) =>async (req, res, next) => {const key = keyFn(req);const cached =await cache.get (key);if (cached) {return res.json ({ ...cached, fromCache:true }); } // Intercept response to cache itconst originalJson = res.json.bind (res); res.json = (data) => { cache.set (key, data, ttl).catch (console.error );return originalJson(data); };next (); }; // Usage on route: // router.get('/', cacheMiddleware(req => `users:page:${req.query.page}`, 120), controller.getAll); module.exports = { cache, cacheMiddleware };
const rateLimit =require ('express-rate-limit' );const RedisStore =require ('rate-limit-redis' );const { client } =require ('./redis' ); // Redis-backed rate limiter (works across multiple servers)const createLimiter = (max, windowMinutes, message) =>rateLimit ({ windowMs: windowMinutes *60 *1000 , max, message: { success:false , message }, standardHeaders:true , legacyHeaders:false , store:new RedisStore({ sendCommand: (...args) => client.call (...args) }), keyGenerator: (req) => req.user?.id || req.ip, // per-user when authenticated }); module.exports = { globalLimiter:createLimiter (100 ,15 ,'Too many requests' ), authLimiter:createLimiter (5 ,15 ,'Too many auth attempts' ), apiLimiter:createLimiter (30 ,1 ,'API rate limit exceeded' ), };
module.exports = {
apps: [{
name: 'vendorflow-api' ,
script: './src/server.js' ,
instances: 'max' , // one per CPU core
exec_mode: 'cluster' , // cluster mode for load balancing
watch: false ,
max_memory_restart: '500M' ,
env: {
NODE_ENV: 'production' ,
PORT: 3000 ,
},
log_date_format: 'YYYY-MM-DD HH:mm:ss' ,
error_file: 'logs/error.log' ,
out_file: 'logs/out.log' ,
merge_logs: true ,
}],
};
# Start, monitor, restart:
# pm2 start ecosystem.config.js --env production
# pm2 monit
# pm2 reload vendorflow-api
# ── Multi-stage Dockerfile ──────────────────────────────
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=deps /app/node_modules ./node_modules
COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "src/server.js"]
version: '3.9'
services:
api:
build: .
ports: ["3000:3000"]
depends_on: [mysql, redis]
env_file: .env
restart: unless-stopped
mysql:
image: mysql:8.0
environment:
MYSQL_DATABASE: vendorflow_db
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
volumes: [mysql_data:/var/lib/mysql]
ports: ["3306:3306"]
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes: [redis_data:/data]
volumes:
mysql_data:
redis_data:
name: Test and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env: { MYSQL_ROOT_PASSWORD: test, MYSQL_DATABASE: test_db }
options: --health-cmd="mysqladmin ping" --health-interval=10s
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npm test -- --coverage
- run: npm run lint
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to VPS
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /app/vendorflow-api
git pull origin main
npm ci --only=production
pm2 reload vendorflow-api
Full REST API with in-memory and file-based storage. Products, categories, search, pagination.
Register/login with JWT, roles, profile management, email verification flow.
Fintech backend: multi-vendor payments, fraud detection hooks, webhook system, admin dashboard API.
vendorflow-api/ ├── src/ │ ├── config/ │ │ ├── app.js # Express + all middleware │ │ ├── database.js # Sequelize connection + pool │ │ ├── redis.js # ioredis client │ │ └── env.js # Validated env vars │ ├── models/ │ │ ├── User.js # id, role, email, kycStatus │ │ ├── Vendor.js # businessName, accountNumber │ │ ├── Payment.js # amount, currency, status, txRef │ │ ├── Wallet.js # balance, currency, userId │ │ └── Webhook.js # url, secret, events │ ├── controllers/ │ │ ├── authController.js │ │ ├── paymentController.js │ │ └── vendorController.js │ ├── services/ │ │ ├── authService.js │ │ ├── paymentService.js # core payment logic │ │ ├── walletService.js # credit/debit + ledger │ │ ├── fraudService.js # velocity checks, rules engine │ │ └── webhookService.js # deliver events to vendors │ ├── routes/ │ │ ├── auth.routes.js │ │ ├── payment.routes.js │ │ ├── vendor.routes.js │ │ └── index.js │ ├── middlewares/ │ │ ├── auth.middleware.js │ │ ├── validate.js │ │ ├── idempotency.js # prevent duplicate payments │ │ └── errorHandler.js │ └── server.js ├── tests/ ├── Dockerfile ├── docker-compose.yml └── .github/workflows/deploy.yml
const { sequelize } =require ('../config/database' );const Payment =require ('../models/Payment' );const Wallet =require ('../models/Wallet' );const fraudService =require ('./fraudService' );const webhookService =require ('./webhookService' );const { ApiError } =require ('../utils/ApiError' );const initiatePayment =async ({ senderId, recipientId, amount, currency, reference }) => { // 1. Fraud check before processingconst fraudCheck =await fraudService.evaluate ({ senderId, amount, currency });if (fraudCheck.blocked) {throw new ApiError(403 ,`Transaction blocked: ${fraudCheck.reason}` ); } // 2. Run in database transaction — atomicity guaranteedconst result =await sequelize.transaction (async (t) => { // Lock wallet rows to prevent race conditionsconst senderWallet =await Wallet.findOne ({ where: { userId: senderId, currency }, lock: t.LOCK .UPDATE, // SELECT FOR UPDATE transaction: t, });if (!senderWallet || senderWallet.balance < amount) {throw new ApiError(422 ,'Insufficient wallet balance' ); }const recipientWallet =await Wallet.findOne ({ where: { userId: recipientId, currency }, lock: t.LOCK .UPDATE, transaction: t, }); // Atomic debit + creditawait senderWallet.decrement ('balance' , { by: amount, transaction: t });await recipientWallet.increment ('balance' , { by: amount, transaction: t });const payment =await Payment.create ({ senderId, recipientId, amount, currency, reference, status:'completed' , riskScore: fraudCheck.riskScore, }, { transaction: t });return payment; }); // 3. Fire webhook asynchronously (don't block response) webhookService.dispatch ('payment.completed' , result).catch (console.error );return result; }; module.exports = { initiatePayment };