A full-stack personal finance application built with Node.js, Express, MongoDB, and Vanilla JavaScript. Covers the complete cycle from secure user authentication to transaction management, custom categories, and interactive data visualization — all without a frontend framework.
This project is a complete expense management system that allows users to:
- Create an account and securely authenticate
- Add, edit, view, and delete financial transactions
- Organize transactions with custom categories (with emoji icons and type filters)
- Search, filter, and paginate through transaction history
- Visualize spending patterns using three Chart.js charts
- Toggle between light and dark mode
- Use the app fully on mobile
It combines a RESTful backend API with a lightweight, responsive frontend — focusing on clean architecture, security best practices, and a polished user experience without reaching for a JS framework.
- User registration and login with password hashing via bcrypt
- JWT-based authentication with protected routes
- MongoDB-backed token blacklist — invalidated tokens persist across server restarts
- Rate limiting on auth routes (10 requests / 15 min) via
express-rate-limit - Zod v4 input validation on all write endpoints — coerces types and returns readable errors
- Add, edit, and delete income or expense transactions
- Category dropdown populated from the user's own saved categories, filtered by transaction type
- Search by description or category (case-insensitive regex)
- Filter by type (income / expense) and date range
- Pagination — configurable page size, returns total pages and navigation flags
- Compound MongoDB indexes on
userId + dateanduserId + typefor fast filtered queries
- Full CRUD — create categories with a name, emoji icon, and type (income / expense / both)
- Categories feed directly into the transaction form dropdown
- Categories page with card grid, skeleton loaders, and empty state
- Doughnut chart — expenses broken down by category
- Bar chart — total income vs expenses vs balance
- Line chart — 6-month income and expense trend
- Date range filter — re-fetches and re-renders all charts on change; chart instances destroyed before redraw to prevent duplicates
- Tailwind CSS v3 (installed locally, not CDN) with a custom
app.cssfor dynamic DOM elements - Dark mode with flash-free initialization (inline
<script>sets class before render) - Hamburger sidebar for mobile with overlay and close button
- Skeleton loaders on all data areas while fetching
- Empty states with contextual messages when no data exists
- Toast notifications (success / error / info) — global
window.showToast() - Confirm-delete modals replacing native
confirm()dialogs - Consistent currency formatting via
window.fmt()usingIntl.NumberFormat(₦) - Fully mobile-responsive layout — fixed sidebar,
md:ml-60content offset, responsive filter bar, stacked forms on small screens
- Serves as a project README/showcase — hero section, feature cards, tech stack badges, full API reference table, and CTA
| Package | Purpose |
|---|---|
| Node.js | Runtime |
| Express.js | HTTP server & routing |
| MongoDB + Mongoose | Database + ODM |
| jsonwebtoken | JWT auth |
| bcrypt | Password hashing |
| Zod v4 | Input validation |
| express-rate-limit | Auth route rate limiting |
| dotenv | Environment config |
| nodemon | Dev auto-restart |
| concurrently | Run server + CSS watcher together |
| Tool | Purpose |
|---|---|
| Vanilla JavaScript | All interactivity |
| Tailwind CSS v3 | Utility-first styling |
| Chart.js | Data visualization |
| Poppins (Google Fonts) | Typography |
Expense-Tracker/
├── config/
│ └── db.js # MongoDB connection
├── controllers/
│ ├── userController.js
│ ├── transactionController.js # Includes filters, pagination, summary
│ └── categoryController.js
├── middleware/
│ ├── auth.js # JWT verify + blacklist check
│ └── validate.js # Reusable Zod validation middleware
├── models/
│ ├── userModel.js
│ ├── transactionModel.js # Compound DB indexes
│ ├── categoryModel.js
│ └── blacklistedTokenModel.js # TTL collection (7d auto-expire)
├── routes/
│ ├── userRoutes.js # Rate-limited auth routes
│ ├── transactionRoutes.js
│ └── categoryRoutes.js
├── validators/
│ ├── userValidators.js
│ ├── transactionValidators.js # Uses z.coerce for HTML form inputs
│ └── categoryValidators.js
├── public/
│ ├── index.html # Landing/showcase page
│ ├── dashboard.html
│ ├── transactions.html
│ ├── summary.html
│ ├── categories.html
│ ├── signIn.html
│ ├── signUp.html
│ ├── css/
│ │ ├── tailwind.input.css # Tailwind source
│ │ ├── tailwind.output.css # Built output (gitignored)
│ │ └── app.css # Tables, modals, toasts, cards, animations
│ └── js/
│ ├── toast.js # Global showToast() + fmt() currency formatter
│ ├── theme.js # Dark mode toggle + hamburger sidebar
│ ├── dashboard.js
│ ├── transactions.js
│ ├── summary.js
│ └── categories.js
├── tailwind.config.js
├── app.js # Express entry point + page routes
└── package.json
- Node.js installed
- MongoDB running locally or via Atlas
git clone https://github.com/HajayB/Expense-Tracker.git
cd Expense-Tracker
npm installTailwind is installed locally. Build the output file before running:
npm run build:cssCreate a .env file in the root:
PORT=3800
MONGO_URI=your_mongodb_connection_string
JWT_SECRET=your_secret_key
# Production
npm start
# Development (nodemon + Tailwind watch in parallel)
npm run devThen open: http://localhost:3800
All endpoints below /api/users/profile, /api/transactions, and /api/categories require:
Authorization: Bearer <token>
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/users/signup |
Register user |
| POST | /api/users/login |
Login user |
| POST | /api/users/logout |
Logout (blacklists token) |
| GET | /api/users/profile |
Get logged-in user profile |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/transactions |
Get transactions — supports ?type, ?search, ?startDate, ?endDate, ?page, ?limit |
| GET | /api/transactions/summary |
Get totals, category breakdown, 6-month trend — supports ?startDate, ?endDate |
| POST | /api/transactions |
Add transaction |
| PUT | /api/transactions/:id |
Edit transaction |
| DELETE | /api/transactions/:id |
Delete transaction |
{
"transactions": [...],
"pagination": {
"total": 42,
"page": 1,
"limit": 10,
"totalPages": 5,
"hasNextPage": true,
"hasPrevPage": false
}
}{
"balance": 150000,
"income": 300000,
"expenses": 150000,
"totalTransactions": 24,
"expensesByCategory": { "Food": 40000, "Transport": 20000 },
"monthly": [
{ "label": "Jan 25", "income": 50000, "expenses": 30000 }
]
}| Method | Endpoint | Description |
|---|---|---|
| GET | /api/categories |
Get all categories for user |
| POST | /api/categories |
Create category |
| PUT | /api/categories/:id |
Update category |
| DELETE | /api/categories/:id |
Delete category |
- Clean MVC separation — routes, controllers, middleware, models, validators each in their own layer
- Real-world auth flow — JWT + bcrypt + MongoDB token blacklist that survives server restarts
- Zod validation with
z.coercehandles the string-to-number mismatch from HTML form inputs without extra parsing - Tailwind scans
.htmland.jsfiles; dynamically built table rows and toasts are styled inapp.csssince Tailwind can't scan JS template literals at build time - Chart instances tracked and destroyed before re-render — prevents memory leaks and duplicate overlays on date range changes
- No frontend framework — all DOM updates, state, and routing done in vanilla JS
Basit Adeola Ajayi GitHub: https://github.com/HajayB