A full-stack expense tracking application built with Node.js + Express backend, React frontend with TypeScript, and SQLite database using Prisma ORM.
- β RESTful API with Express.js
- β SQLite database with Prisma ORM
- β Idempotency support for safe duplicate request handling
- β Input validation and error handling
- β Clean architecture (routes β controllers β services β repository)
- β Money handling using paise (integers) to avoid floating-point errors
- β CORS support for development
- β React 18 with TypeScript
- β Vite bundler for fast development
- β Add expenses with category, amount, description, date
- β Filter expenses by category
- β Sort by date (newest/oldest first)
- β Real-time total calculation
- β Loading and error states
- β Responsive design
- β Idempotency-key header support
backend/
βββ src/
β βββ index.ts # Express app setup, Prisma client
β βββ middleware/
β β βββ errorHandler.ts # Error handling & logging
β β βββ idempotency.ts # Idempotency middleware
β βββ routes/
β β βββ expenses.ts # Route definitions
β βββ controllers/
β β βββ expenseController.ts # Request handlers
β βββ services/
β β βββ expenseService.ts # Business logic
β βββ repository/
β β βββ expenseRepository.ts # Database access
β βββ utils/
β βββ validation.ts # Input validation & money conversion
βββ prisma/
β βββ schema.prisma # Database schema
βββ package.json
βββ tsconfig.json
frontend/
βββ src/
β βββ components/
β β βββ ExpenseForm.tsx # Add expense form
β β βββ ExpenseList.tsx # Display expenses table
β β βββ ExpenseControls.tsx # Filter & sort controls
β βββ hooks/
β β βββ useExpenses.ts # API calls & state management
β βββ types/
β β βββ index.ts # TypeScript interfaces
β βββ styles/
β β βββ App.css
β β βββ ExpenseForm.css
β β βββ ExpenseList.css
β β βββ ExpenseControls.css
β β βββ index.css
β βββ App.tsx # Main app component
β βββ main.tsx # Entry point
β βββ App.css
βββ vite.config.ts
βββ index.html
βββ package.json
βββ tsconfig.json
- Decision: Store amounts as integers in paise (βΉ1 = 100 paise)
- Why: Avoids floating-point precision errors common in financial applications
- Trade-off: API and frontend work with both paise (storage) and rupees (display)
- Implementation: Conversion utilities in
validation.ts
- Decision: Implemented idempotency using
Idempotency-Keyheader - How:
- Store request hash and response in
IdempotencyKeytable - On retry with same key, return cached response
- 24-hour expiration for cleanup
- Store request hash and response in
- Trade-off: Adds database overhead but ensures data integrity
- Use Case: Handles network retries, duplicate form submissions
- Decision: Separate concerns into routes β controllers β services β repository
- Why:
- Testability: Each layer can be tested independently
- Maintainability: Easy to modify business logic without touching routes
- Reusability: Services can be used across multiple routes
- Trade-off: More files for small CRUD operations, but scales well
- Decision:
useExpenses,useCreateExpense,useExpenseSummaryhooks - Why:
- Reusable API logic
- Centralized error handling
- Consistent loading states
- Trade-off: Could use React Query, but custom hooks are lightweight and sufficient
- Decision: SQLite for local development, Prisma as ORM
- Why:
- SQLite: No setup required, file-based, perfect for prototyping
- Prisma: Type-safe, migrations built-in, excellent DX
- Trade-off: SQLite not suitable for multi-server deployment (use PostgreSQL in production)
- Decision: Allow
http://localhost:5173(Vite dev server) - Why: Needed for local development
- Trade-off: Must be changed for production (use environment-based configuration)
Create a new expense (idempotent)
Headers:
Content-Type: application/json
Idempotency-Key: <uuid> # Required for idempotency
Request Body:
{
"amount": 50000, // in paise (βΉ500)
"category": "Food",
"description": "Lunch",
"date": "2024-04-28T12:00:00Z"
}Response (201):
{
"success": true,
"data": {
"id": "uuid",
"amount": 500, // in rupees
"amountPaise": 50000, // in paise
"category": "Food",
"description": "Lunch",
"date": "2024-04-28T12:00:00Z",
"createdAt": "2024-04-28T12:00:00Z"
}
}Get expenses with filtering and sorting
Query Parameters:
GET /expenses?category=Food&sort=date_desc
Response (200):
{
"success": true,
"data": [...],
"summary": {
"count": 5,
"total": 2500
}
}Get expense summary by category
Response (200):
{
"success": true,
"data": [
{
"category": "Food",
"total": 1500,
"totalPaise": 150000,
"count": 3
}
]
}- β Amount must be > 0
- β Category is required (non-empty string)
- β Date must be valid ISO string
- β Description optional but must be string if provided
{
"error": "Validation failed",
"details": [
{ "field": "amount", "message": "Amount must be greater than 0" }
]
}- Node.js 16+ (LTS recommended)
- npm or yarn
-
Navigate to backend directory:
cd backend -
Install dependencies:
npm install
-
Setup Prisma database:
npm run prisma:generate npm run prisma:migrate
-
Start development server:
npm run dev
Server runs on
http://localhost:3000
-
Navigate to frontend directory:
cd ../frontend -
Install dependencies:
npm install
-
Start development server:
npm run dev
Frontend runs on
https://fenmo-blue.vercel.app
- Open
https://fenmo-blue.vercel.appin browser - Fill the expense form:
- Amount (in rupees)
- Category (Food, Transport, etc.)
- Description (optional)
- Date
- Click "Add Expense"
- View expenses in table
- Filter by category or sort by date
model Expense {
id String @id @default(uuid())
amount Int // in paise
category String @db.Text
description String @db.Text
date DateTime
createdAt DateTime @default(now())
}
model IdempotencyKey {
id String @id @default(uuid())
key String @unique
requestHash String @db.Text
response String @db.Text
createdAt DateTime @default(now())
expiresAt DateTime
}- Current: 24 hours
- Trade-off: Could be shorter for high-traffic apps, but 24h safe for most use cases
- Future: Implement background job for cleanup
- Current: Generic error messages for security
- Trade-off: Less helpful for debugging in production
- Solution: Enable detailed logging only in development
- Current: React hooks + custom API hooks
- Trade-off: Simpler than Redux/Zustand, but less structured for large apps
- Future: Consider Redux if app scales significantly
- Current: Fetched dynamically from expenses
- Trade-off: No separate category table
- Future: Add predefined categories in database for consistency
This app treats money as integers (paise) to avoid floating-point arithmetic issues:
- βΉ500 = 50,000 paise
- βΉ1.50 = 150 paise
This is a best practice in financial systems and eliminates rounding errors.
The Idempotency-Key header allows clients to safely retry requests:
- Same key + same request = cached response (no duplicate expense)
- Different key = new expense created
- Expires after 24 hours for database cleanup
- Routes: Define endpoints and HTTP methods
- Controllers: Handle request/response, call services
- Services: Contain business logic, format responses
- Repository: Database operations, single source of truth
This structure makes the app:
- Easy to test (mock each layer independently)
- Easy to extend (add new features without touching existing code)
- Easy to understand (clear separation of concerns)