A RESTful backend API for managing users, accounts, and financial transactions using Node.js, Express, MongoDB, and Mongoose.
The project uses a double-entry ledger approach, where account balances are calculated from immutable debit and credit ledger entries rather than being stored and directly updated as a balance.
- User registration and login
- JWT-based authentication
- HTTP-only cookie authentication
- Token blacklist-based logout
- Password hashing with bcrypt
- User account creation and retrieval
- Account balance calculation from ledger entries
- Credit and debit ledger entries
- Atomic transactions using MongoDB sessions
- Transaction idempotency using unique idempotency keys
- Transaction status tracking
- System-user authorization for initial funds
- Immutable ledger entries
- Automatic expiration of blacklisted JWTs
- Node.js
- Express.js
- MongoDB
- Mongoose
- JWT (JSON Web Tokens)
- bcryptjs
- cookie-parser
- dotenv
ledger-api/
├── server.js
├── package.json
├── .env.example
└── src/
├── app.js
├── config/
│ └── db.js
├── controllers/
│ ├── account.controller.js
│ ├── auth.controller.js
│ └── transaction.controller.js
├── middleware/
│ └── auth.middleware.js
├── models/
│ ├── account.model.js
│ ├── blackList.model.js
│ ├── ledger.model.js
│ ├── transaction.model.js
│ └── user.model.js
└── routes/
├── account.route.js
├── auth.routes.js
└── transaction.routes.js
The application follows a simple layered architecture:
Client
│
▼
Express Routes
│
▼
Authentication Middleware
│
▼
Controllers
│
▼
Mongoose Models
│
▼
MongoDB
Account balances are not stored as a mutable balance field.
Instead, the balance is calculated from ledger entries:
Balance = Total Credits - Total Debits
For example:
Account A
Credit +1000
Debit -250
Credit +500
----------------
Balance = 1250
This provides an auditable transaction history and avoids directly mutating an account's balance.
Stores application users.
Important fields:
nameemailpasswordsystemUser
Passwords are hashed with bcryptjs before being stored.
Represents a user's financial account.
Important fields:
userstatuscurrency
Supported account statuses:
ACTIVEFROZENCLOSED
The default currency is INR.
Represents a transfer between two accounts.
Important fields:
fromAccounttoAccountamountstatusidempotencyKey
Supported transaction statuses:
PENDINGCOMPLETEDFAILEDREVERSED
Represents an individual financial movement.
Each transaction creates:
- A
DEBITledger entry for the source account - A
CREDITledger entry for the destination account
Ledger entries are intentionally immutable and cannot be updated or deleted through the model's protected operations.
Used to invalidate JWTs after logout.
Blacklisted tokens automatically expire from MongoDB after three days.
Make sure you have the following installed:
- Node.js
- npm
- MongoDB
A MongoDB Atlas database can also be used.
Clone the repository:
git clone <repository-url>
cd backend-ledgerInstall dependencies:
npm installCreate a .env file in the project root:
MONGO_URI=mongodb://localhost:27017/ledger-api
JWT_SECRET=your-super-secret-jwt-key
PORT=3000You can use .env.example as a starting point.
Never commit your
.envfile or production secrets to version control.
npm run devnpm startThe server runs on:
http://localhost:3000
The API is organized into three main resources:
/api/auth
/api/accounts
/api/transactions
POST /api/auth/registerRequest body:
{
"name": "John Doe",
"email": "john@example.com",
"password": "password123"
}Response:
{
"user": {
"_id": "USER_ID",
"name": "John Doe",
"email": "john@example.com"
},
"token": "JWT_TOKEN"
}POST /api/auth/loginRequest body:
{
"email": "john@example.com",
"password": "password123"
}POST /api/auth/logoutThe current JWT is added to the blacklist and the authentication cookie is cleared.
All account endpoints require authentication.
Authentication can be provided through the HTTP-only token cookie or an Authorization header:
Authorization: Bearer <JWT_TOKEN>POST /api/accountsCreates an account for the authenticated user.
GET /api/accountsReturns all accounts belonging to the authenticated user.
GET /api/accounts/balance/:accountIdReturns the current balance calculated from the account's ledger entries.
Example response:
{
"accountId": "ACCOUNT_ID",
"message": "Account balance retrieved successfully",
"balance": 1000
}POST /api/transactionsCreates a transfer between two accounts belonging to the authenticated user.
Request body:
{
"fromAccount": "SOURCE_ACCOUNT_ID",
"toAccount": "DESTINATION_ACCOUNT_ID",
"amount": 250,
"idempotencyKey": "unique-request-id-123"
}When a transaction is created:
Validate request
│
▼
Verify source & destination accounts
│
▼
Check idempotency key
│
▼
Verify accounts are ACTIVE
│
▼
Check available balance
│
▼
Start MongoDB transaction
│
├── Create transaction
│
├── Create DEBIT ledger entry
│
├── Create CREDIT ledger entry
│
└── Mark transaction COMPLETED
│
▼
Commit MongoDB transaction
If an error occurs during the database transaction, the MongoDB transaction is aborted.
Every transaction requires an idempotencyKey.
The key prevents the same request from being processed multiple times.
For example, if a client sends:
{
"fromAccount": "ACCOUNT_A",
"toAccount": "ACCOUNT_B",
"amount": 500,
"idempotencyKey": "payment-12345"
}and retries the exact request using the same key, the backend recognizes the existing transaction instead of creating another one.
This is particularly useful when clients retry requests because of network failures or timeouts.
The API also provides a system-user-only endpoint for creating initial funds:
POST /api/transactions/system/initial-fundsRequest body:
{
"toAccount": "ACCOUNT_ID",
"amount": 10000,
"idempotencyKey": "initial-funds-001"
}This endpoint requires the authenticated user to have:
systemUser = true
The operation creates the corresponding debit and credit ledger entries inside a MongoDB transaction.
JWTs are generated during registration and login.
Tokens:
- Expire after 3 days
- Are stored in an HTTP-only cookie
- Can also be supplied through the
Authorizationheader - Are checked against the token blacklist
Example:
Authorization: Bearer <JWT_TOKEN>There are two authentication middleware levels:
Used for normal authenticated users.
authMiddleware
Used for privileged system operations.
authSystemUserMiddleware
System-user middleware verifies both the JWT and the user's systemUser flag.
Financial operations use MongoDB sessions and transactions to keep related database changes atomic.
A normal transfer consists of multiple writes:
Transaction record
+
Debit ledger entry
+
Credit ledger entry
These operations are committed together.
If any operation fails:
MongoDB Transaction
│
├── Transaction ❌
├── Debit ❌
└── Credit ❌
The transaction is aborted so that partial ledger records are not committed.
The API returns appropriate HTTP status codes for common failures.
Examples:
| Status | Meaning |
|---|---|
200 |
Successful request |
201 |
Resource created |
400 |
Invalid request / insufficient balance |
401 |
Authentication required or invalid |
403 |
Insufficient privileges |
404 |
Resource not found |
422 |
User already exists |
500 |
Transaction/server failure |
Example error:
{
"message": "Insufficient balance in from account"
}The project includes several security mechanisms:
- Password hashing with bcrypt
- JWT authentication
- HTTP-only authentication cookies
- JWT expiration
- Token blacklisting on logout
- System-user authorization
- User-specific account access checks
- Idempotency protection for transactions
- Immutable ledger records
- MongoDB atomic transactions
For production deployment, additional protections such as HTTPS, secure cookie configuration, rate limiting, input validation, logging, monitoring, and secret management should be added.
| Command | Description |
|---|---|
npm install |
Install dependencies |
npm run dev |
Start development server with Nodemon |
npm start |
Start the production server |
Suppose:
Account A balance = ₹1,000
Account B balance = ₹500
A transfer of ₹300 from A to B produces:
Account A
DEBIT ₹300
Balance = ₹700
Account B
CREDIT ₹300
Balance = ₹800
The ledger records remain available as the source of truth for calculating balances.
Potential improvements include:
- Request validation using a schema validation library
- Centralized error-handling middleware
- Transaction history endpoints
- Pagination for accounts and transactions
- Account creation with configurable currency
- Account ownership rules for transfers
- Better transaction reversal workflows
- Rate limiting
- Structured application logging
- API documentation with OpenAPI/Swagger
- Automated unit and integration tests
- Docker support
- Production-ready security configuration
- Proper role/permission management
- Audit logging
- Improved concurrency handling around balance checks
This project currently uses the ISC license as specified in package.json.