A production-grade banking transaction system backend built with Node.js, Express, and MongoDB — designed to mirror real-world banking architectures with a focus on data consistency, auditability, and secure financial operations.
🎯 Built using a Ledger-based architecture — balances are never hardcoded, they are always derived from actual transaction history, guaranteeing full data integrity.
🌐 Live API: https://backend-ledger-f0dy.onrender.com
- Architecture Overview
- Features
- Tech Stack
- Project Structure
- Getting Started
- Environment Variables
- API Reference
- How the Ledger System Works
- Idempotency Keys
- License
Client Request
│
▼
Express Router
│
▼
Auth Middleware (JWT Verify)
│
▼
Controller
│
▼
Service Layer
│
▼
MongoDB (Atlas)
┌──────────────────────────────────────┐
│ Users │ Accounts │ Transactions │
│ │ Ledger Entries │
└──────────────────────────────────────┘
│
▼
Nodemailer (Gmail OAuth)
[Email Alerts on Account Events]
The project follows a modular MVC-style architecture with clear separation between routes, controllers, services, and models.
- 🔐 JWT Authentication with bcrypt password hashing
- 📧 Email Notifications via Nodemailer + Google Gmail OAuth
- 🏦 Multi-Account Support per user
- 📒 Ledger-Based Balance Calculation — no hardcoded balances, ever
- 🔁 Idempotency Keys — prevents duplicate transactions on retry
- 📊 MongoDB Aggregation Pipeline — dynamic balance computation
- 🛡️ Auth Middleware on all protected routes
- 🔑 System-level access control for fund injection
- ☁️ Deployed on Render with secured environment variables
| Layer | Technology |
|---|---|
| Runtime | Node.js |
| Framework | Express.js |
| Database | MongoDB Atlas + Mongoose |
| Authentication | JWT + bcrypt |
| Email Service | Nodemailer + Google Gmail API (OAuth2) |
| Deployment | Render |
ledger-api/
├── controllers/
│ ├── authController.js
│ ├── accountController.js
│ └── transactionController.js
├── models/
│ ├── User.js
│ ├── Account.js
│ ├── Transaction.js
│ └── Ledger.js
├── routes/
│ ├── authRoutes.js
│ ├── accountRoutes.js
│ └── transactionRoutes.js
├── middleware/
│ ├── authMiddleware.js
│ └── authSystemUserMiddleware.js
├── services/
│ └── emailService.js
├── .env
├── app.js
└── server.js
- Node.js v18+
- MongoDB Atlas account
- Google Cloud project with Gmail API enabled (for email notifications)
# Clone the repository
git clone https://github.com/your-username/ledger-api.git
cd ledger-api
# Install dependencies
npm install
# Setup environment variables
cp .env.example .env
# Fill in your values in .env
# Start development server
npm run devCreate a .env file in the root directory:
PORT=3000
# MongoDB
MONGO_URI=mongodb+srv://<username>:<password>@cluster.mongodb.net/ledger
# JWT
JWT_SECRET=your_super_secret_jwt_key
# System User (for initial funds injection)
SYSTEM_SECRET=your_system_level_secret
# Google Gmail OAuth2 (Nodemailer)
GMAIL_CLIENT_ID=your_google_client_id
GMAIL_CLIENT_SECRET=your_google_client_secret
GMAIL_REFRESH_TOKEN=your_refresh_token
GMAIL_USER=your_email@gmail.com
⚠️ Never commit your.envfile. Add it to.gitignore.
Protected routes require the
Authorization: Bearer <token>header.
Base: /api/v1/auth
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /register |
❌ | Register a new user |
| POST | /login |
❌ | Login and receive JWT |
| POST | /logout |
❌ | Logout user |
POST /api/v1/auth/register
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com",
"password": "secret123"
}Response:
{
"success": true,
"message": "User registered successfully",
"token": "<jwt_token>"
}POST /api/v1/auth/login
Content-Type: application/json
{
"email": "john@example.com",
"password": "secret123"
}Response:
{
"success": true,
"token": "<jwt_token>"
}📧 Email Notification on Registration

Base: /api/v1/account
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | / |
✅ User | Create a new bank account |
| GET | / |
✅ User | Get all accounts of logged-in user |
| GET | /balance/:accountId |
✅ User | Get real-time balance via ledger aggregation |
💡 Balance is computed dynamically using MongoDB Aggregation Pipeline by summing all credit and debit ledger entries — not fetched from a static field.
POST /api/v1/account
Authorization: Bearer <token>
Content-Type: application/json
{
"accountName": "Savings Account"
}GET /api/v1/account/balance/:accountId
Authorization: Bearer <token>Response:
{
"success": true,
"accountId": "abc123",
"balance": 15000
}Base: /api/v1/transaction
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /transfer |
✅ User | Transfer funds between accounts |
| POST | /initial-funds |
✅ System | Inject initial funds into an account |
POST /api/v1/transaction/transfer
Authorization: Bearer <token>
Content-Type: application/json
{
"fromAccountId": "abc123",
"toAccountId": "xyz789",
"amount": 500,
"idempotencyKey": "unique-key-12345"
}Response:
{
"success": true,
"message": "Transfer successful",
"transactionId": "txn_98765"
}POST /api/v1/transaction/initial-funds
Authorization: Bearer <system_token>
Content-Type: application/json
{
"accountId": "abc123",
"amount": 10000
}Instead of storing a balance field directly on an account, every financial event is stored as a Ledger Entry:
Account Created
│
▼
Initial Funds Added → Ledger Entry { type: "credit", amount: 10000 }
│
▼
Transfer of ₹500 → Ledger Entry { type: "debit", amount: 500, fromAccount: A }
→ Ledger Entry { type: "credit", amount: 500, toAccount: B }
│
▼
Balance Query
= SUM(all credits) - SUM(all debits)
= Calculated via MongoDB Aggregation Pipeline
Why this matters:
- ✅ Full audit trail of every rupee
- ✅ Balance is always derivable from history
- ✅ No risk of balance going out of sync
- ✅ Mirrors how real banks operate
To prevent duplicate transactions (e.g., if a client retries a failed request), every transfer request accepts an idempotencyKey.
Client sends Transfer Request with idempotencyKey: "order-pay-001"
│
▼
Server checks: Has this key been processed before?
│
├── YES → Return original response (no duplicate transaction)
│
└── NO → Process transaction, store key, return response
This is a critical requirement in real-world payment systems to ensure that network retries don't result in double charges.
This API is deployed on Render.
Key deployment practices followed:
- All secrets stored as environment variables on Render dashboard
node_modules/and.envexcluded via.gitignore- MongoDB Atlas connection string whitelisted for Render's IP range
MIT © Priyanshu Varshney
Built with ❤️ following production-grade banking architecture principles







