A Node.js + Express + MongoDB backend for a ledger-based banking workflow.
This project provides:
- User authentication with JWT and cookie support
- Account creation and balance lookup
- Double-entry ledger transactions (debit + credit)
- Idempotency protection for transaction creation
- Token blacklist-based logout
- Email notifications for registration and transactions
- Node.js
- Express
- MongoDB + Mongoose
- JWT (
jsonwebtoken) - Cookies (
cookie-parser) - Password hashing (
bcrypt) - Email (
nodemailerwith Gmail OAuth2)
Banking Ledger/
package.json
server.js
src/
app.js
config/
db.js
controller/
auth.controller.js
account.controller.js
transaction.controller.js
middleware/
auth.middleware.js
models/
user.model.js
account.model.js
ledger.model.js
transactiion.model.js
blackList.model.js
routes/
auth.routes.js
account.routes.js
transaction.routes.js
services/
email.service.js
POST /api/auth/registercreates a user and returns a JWT.POST /api/auth/loginvalidates credentials and returns a JWT.- JWT is sent in response and also set in a
tokencookie. - Protected routes read token from either:
- Cookie:
token - Header:
Authorization: Bearer <token>
- Cookie:
- Each account belongs to a user.
- Account has
status(ACTIVE,FROZEN,CLOSED) andcurrency(defaultINR). - Balance is derived from ledger entries:
- Credits increase balance
- Debits decrease balance
For each transfer:
- A transaction document is created (
PENDING->COMPLETED). - One
DEBITledger entry is created for sender account. - One
CREDITledger entry is created for receiver account. - The operation runs in a MongoDB session/transaction.
- Each transaction requires a unique
idempotencyKey. - If the key already exists, API returns a status-aware response instead of duplicating transfer.
- Logout stores token in blacklist collection.
- Blacklisted tokens are rejected by auth middleware.
- Blacklist documents expire automatically (TTL index: 3 days).
- Node.js 18+
- MongoDB connection URI
- Gmail OAuth2 credentials (for email notifications)
npm installCreate a .env file in project root:
MONGO_URI=your_mongodb_connection_string
JWT_SECRET=your_jwt_secret
EMAIL_USER=your_email@gmail.com
CLIENT_ID=your_google_oauth_client_id
CLIENT_SECRET=your_google_oauth_client_secret
REFRESH_TOKEN=your_google_oauth_refresh_tokenDevelopment:
npm run devProduction:
npm startServer starts on:
http://localhost:2000
http://localhost:2000/api
POST /auth/register
Request body:
{
"name": "Naman",
"email": "naman@example.com",
"password": "password123"
}POST /auth/login
Request body:
{
"email": "naman@example.com",
"password": "password123"
}POST /auth/logout
Auth required.
POST /accounts
Auth required.
GET /accounts
Auth required.
GET /accounts/balance/:accountId
Auth required.
POST /transactions
Auth required.
Request body:
{
"fromAccount": "ACCOUNT_ID_1",
"toAccount": "ACCOUNT_ID_2",
"amount": 500,
"idempotencyKey": "txn-2026-03-16-0001"
}POST /transactions/system/initial-funds
Requires authenticated user with systemUser=true.
Request body:
{
"toAccount": "ACCOUNT_ID",
"amount": 1000,
"idempotencyKey": "init-funds-2026-03-16-0001"
}- Send token in cookie or bearer header.
- Blacklisted tokens are denied.
- Some routes require system-user authorization.
Bearer header format:
Authorization: Bearer <jwt_token>nameemail(unique)password(hashed, hidden by default)systemUser(defaultfalse, hidden by default)
user(ObjectId -> User)status(ACTIVE | FROZEN | CLOSED)currency(defaultINR)
fromAccount(ObjectId -> Account)toAccount(ObjectId -> Account)amountstatus(PENDING | COMPLETED | FAILED | REVERSED)idempotencyKey(unique)
account(ObjectId -> Account)transaction(ObjectId -> Transaction)amounttype(DEBIT | CREDIT)- Immutable after creation (update/delete operations are blocked)
token(unique)- TTL expiration (3 days)
Register:
curl -X POST http://localhost:2000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"name":"Naman","email":"naman@example.com","password":"password123"}'Create account (Bearer token):
curl -X POST http://localhost:2000/api/accounts \
-H "Authorization: Bearer YOUR_TOKEN"Create transaction:
curl -X POST http://localhost:2000/api/transactions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"fromAccount":"ACCOUNT_ID_1","toAccount":"ACCOUNT_ID_2","amount":500,"idempotencyKey":"txn-001"}'ISC