Node.js + Express + MongoDB based ledger service that supports:
- User register/login/logout (JWT stored in cookie, token also returned in response)
- Creating accounts for logged-in users
- Getting account balance derived from immutable ledger entries
- Creating transfers with idempotency protection
- (Optional) “system user” endpoint for initial-funds credit
Note: This repo contains only the backend.
- Node.js (LTS recommended)
- Express
- MongoDB + Mongoose
- JWT auth (cookie +
Authorization: Bearer <token>supported) - Nodemailer (Gmail OAuth2)
Backend/
server.js
package.json
src/
app.js
config/db.js
controllers/
middleware/
models/
routes/
services/
From project root:
cd Backend
npm installCreate a .env file inside the Backend/ folder (same level as server.js).
Minimum required:
MONGO_URI=mongodb://127.0.0.1:27017/backend-ledger
JWT_SECRET=your-super-secretEmail (optional but recommended; the code attempts to connect on startup):
EMAIL_USER=your-email@gmail.com
CLIENT_ID=google-oauth-client-id
CLIENT_SECRET=google-oauth-client-secret
REFRESH_TOKEN=google-oauth-refresh-tokenIf you don’t want email during local testing, you can still run the server, but you may see email connection errors in logs.
# dev (nodemon)
npm run dev
# production
npm startServer runs on:
http://localhost:3000
Health check:
GET /→Ledger Service is up and running
On successful register/login:
- A JWT is set as cookie
token - The same token is also returned in JSON
Protected routes accept token via:
- Cookie:
token=<jwt> - Header:
Authorization: Bearer <jwt>
Logout (POST /api/auth/logout) blacklists the token in MongoDB for ~3 days (TTL index).
Base URL: http://localhost:3000
POST /api/auth/register
Body:
{ "email": "user@example.com", "password": "secret123", "name": "User" }POST /api/auth/login
Body:
{ "email": "user@example.com", "password": "secret123" }POST /api/auth/logout
POST /api/accounts/
Response returns the created account document.
GET /api/accounts/
GET /api/accounts/balance/:accountId
Balance is computed from ledger aggregation: balance = totalCredit - totalDebit.
POST /api/transactions/
Body:
{
"fromAccount": "<accountId>",
"toAccount": "<accountId>",
"amount": 100,
"idempotencyKey": "unique-string-per-request"
}Notes:
idempotencyKeymust be unique. If the same key is reused:- returns the existing COMPLETED transaction
- or returns “still processing” for PENDING
- Implementation intentionally includes a ~15s delay to simulate processing.
POST /api/transactions/system/initial-funds
Body:
{
"toAccount": "<accountId>",
"amount": 500,
"idempotencyKey": "unique-string-per-request"
}This route requires the authenticated user to have systemUser: true.
curl -i -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d "{\"email\":\"user@example.com\",\"password\":\"secret123\",\"name\":\"User\"}"curl -i -c cookies.txt -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d "{\"email\":\"user@example.com\",\"password\":\"secret123\"}"curl -i -b cookies.txt -X POST http://localhost:3000/api/accounts/curl -i -b cookies.txt http://localhost:3000/api/accounts/curl -i -b cookies.txt -X POST http://localhost:3000/api/transactions/ \
-H "Content-Type: application/json" \
-d "{\"fromAccount\":\"FROM_ID\",\"toAccount\":\"TO_ID\",\"amount\":100,\"idempotencyKey\":\"txn-001\"}"- Port is currently hard-coded to
3000inBackend/server.js. - Ledger entries are designed to be immutable (update/delete hooks throw errors).
- Token blacklist uses a TTL index of ~3 days.
- MongoDB connection fails: verify
MONGO_URIand that MongoDB is running. - 401 Unauthorized on protected routes: pass cookie
tokenorAuthorization: Bearer <token>. - Email server errors: verify Gmail OAuth2 env vars, or ignore during local testing.