Detaillierte technische Architektur und Implementierungsdetails des Budget Trackers.
- Systemarchitektur
- Frontend-Architektur
- Backend-Architektur
- Datenbank-Design
- State Management
- API-Integration
- Styling & UI
- Performance-Optimierungen
Der Budget Tracker folgt einer klassischen 3-Tier-Architektur:
┌─────────────────────────────────────────┐
│ Presentation Layer │
│ React Frontend (Port 3000) │
│ - Components │
│ - State Management │
│ - UI/UX │
└─────────────┬───────────────────────────┘
│ HTTP REST API
│ (Axios)
┌─────────────▼───────────────────────────┐
│ Application Layer │
│ Node.js + Express (Port 5000) │
│ - Routes │
│ - Controllers │
│ - Business Logic │
└─────────────┬───────────────────────────┘
│ Mongoose ODM
│
┌─────────────▼───────────────────────────┐
│ Data Layer │
│ MongoDB Atlas (Cloud) │
│ - Collections │
│ - Schemas │
│ - Indexes │
└─────────────────────────────────────────┘
| Technologie | Version | Verwendung |
|---|---|---|
| React | 18.x | UI Framework |
| Axios | 1.13.2 | HTTP Client |
| JavaScript (ES6+) | - | Programmiersprache |
| CSS3 | - | Styling |
| Technologie | Version | Verwendung |
|---|---|---|
| Node.js | 16+ | Runtime Environment |
| Express.js | 4.x | Web Framework |
| Mongoose | 7.x | MongoDB ODM |
| CORS | 2.x | Cross-Origin Requests |
| dotenv | 16.x | Environment Variables |
| Technologie | Version | Verwendung |
|---|---|---|
| MongoDB Atlas | 6.0+ | Cloud Database |
| MongoDB Compass | - | GUI (optional) |
frontend/
├── public/
│ ├── index.html # HTML Template
│ ├── favicon.ico # App Icon
│ ├── logo192.png # Logo
│ ├── logo512.png # Logo
│ ├── manifest.json # PWA Manifest
│ └── robots.txt # SEO
│
├── src/
│ ├── components/
│ │ └── Dashboard.js # Haupt-Komponente (alle Sub-Components)
│ │
│ ├── App.js # Root Component
│ ├── App.css # Global Styles
│ ├── index.js # Entry Point
│ ├── index.css # Base Styles
│ │
│ ├── reportWebVitals.js # Performance Monitoring
│ └── setupTests.js # Test Configuration
│
├── package.json # Dependencies
└── README.md # Frontend-Docs
App
└── Dashboard
├── TransactionForm
├── TransactionList
├── SavingsGoalForm
└── SavingsGoalCard
Datei: src/components/Dashboard.js
Verantwortlichkeiten:
- State Management für gesamte App
- API-Calls (CRUD-Operationen)
- Daten-Aggregation (Berechnungen)
- Layout-Struktur
State:
const [transactions, setTransactions] = useState([]); // Alle Transaktionen
const [savingsGoals, setSavingsGoals] = useState([]); // Alle Sparziele
const [incomeTotal, setIncomeTotal] = useState(0); // Einnahmen-Summe
const [expenseTotal, setExpenseTotal] = useState(0); // Ausgaben-Summe
const [balance, setBalance] = useState(0); // Saldo
const [searchTerm, setSearchTerm] = useState(''); // Such-Term
const [filterType, setFilterType] = useState('all'); // FilterLifecycle:
useEffect(() => {
fetchTransactions(); // Initial Load
fetchSavingsGoals(); // Initial Load
}, []); // Nur beim MountBesonderheiten:
- Alle Sub-Komponenten sind im selben File (kein separates Import nötig)
- Inline-Styles für schnelle Entwicklung
- Functional Component mit Hooks
Props:
onAdd: Function- Callback zum HinzufügensavingsGoals: Array- Liste der Sparziele (ungenutzt, für spätere Features)
State:
const [text, setText] = useState(''); // Beschreibung
const [amount, setAmount] = useState(''); // Betrag
const [type, setType] = useState('expense'); // Typ (income/expense)Validierung:
if (text && amount) {
onAdd({ text, amount: parseFloat(amount), type });
// Reset Form
}Props:
transactions: Array- Anzuzeigende TransaktionenonDelete: Function- Callback zum Löschen
Features:
- Iteration über Transaktionen
- Datum-Formatierung:
new Date(txn.createdAt).toLocaleDateString('de-CH') - Farbcodierung: Grün für Income, Rot für Expense
Props:
onAdd: Function- Callback zum Erstellen
State:
const [name, setName] = useState(''); // Name des Ziels
const [targetAmount, setTargetAmount] = useState(''); // ZielbetragSubmit:
onAdd({
name,
targetAmount: parseFloat(targetAmount),
currentAmount: 0
});Props:
goal: Object- Sparziel-ObjektonUpdate: Function- Callback zum AktualisierenonDelete: Function- Callback zum Löschen
State:
const [isEditing, setIsEditing] = useState(false); // Edit-Modus
const [amount, setAmount] = useState(''); // Neuer BetragFortschritts-Berechnung:
const progress = (goal.currentAmount / goal.targetAmount) * 100;Features:
- Inline-Editing (Toggle zwischen Anzeige und Edit)
- Fortschrittsbalken mit dynamischer Breite
- Farb-Wechsel bei 100%
backend/
├── models/
│ ├── Transaction.js # Transaction Schema
│ └── SavingsGoal.js # SavingsGoal Schema
│
├── routes/
│ ├── transactions.js # Transaction Routes
│ └── savingsGoals.js # SavingsGoal Routes
│
├── server.js # Entry Point
├── .env # Environment Variables
├── .gitignore # Git Ignore
└── package.json # Dependencies
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
const app = express();
// Middleware
app.use(cors());
app.use(express.json());
// Routes
app.use('/api/transactions', require('./routes/transactions'));
app.use('/api/savings-goals', require('./routes/savingsGoals'));
// MongoDB Connection
mongoose.connect(process.env.MONGODB_URI)
.then(() => console.log('MongoDB verbunden!'))
.catch(err => console.error('MongoDB Fehler:', err));
// Server starten
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server läuft auf Port ${PORT}`);
});- CORS - Erlaubt Requests von Frontend
- express.json() - Parst JSON-Body
- Routes - Routing-Handler
- Error Handler - Fehlerbehandlung (implicit)
Schema:
const TransactionSchema = new mongoose.Schema({
text: {
type: String,
required: [true, 'Beschreibung ist erforderlich'],
trim: true
},
amount: {
type: Number,
required: [true, 'Betrag ist erforderlich'],
min: [0, 'Betrag muss positiv sein']
},
type: {
type: String,
required: true,
enum: {
values: ['income', 'expense'],
message: 'Typ muss income oder expense sein'
}
}
}, {
timestamps: true // createdAt, updatedAt
});Beispiel-Dokument:
{
"_id": ObjectId("65a1b2c3d4e5f6g7h8i9j0k1"),
"text": "Gehalt Januar",
"amount": 5000,
"type": "income",
"createdAt": ISODate("2026-01-15T08:30:00.000Z"),
"updatedAt": ISODate("2026-01-15T08:30:00.000Z"),
"__v": 0
}Indexes:
_id- Automatic (Primary Key)createdAt- Für Sortierung (optional)
Schema:
const SavingsGoalSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name ist erforderlich'],
trim: true
},
targetAmount: {
type: Number,
required: [true, 'Zielbetrag ist erforderlich'],
min: [0, 'Zielbetrag muss positiv sein']
},
currentAmount: {
type: Number,
default: 0,
min: [0, 'Aktueller Betrag kann nicht negativ sein']
}
}, {
timestamps: true
});Beispiel-Dokument:
{
"_id": ObjectId("65b1c2d3e4f5g6h7i8j9k0l1"),
"name": "Urlaub",
"targetAmount": 5000,
"currentAmount": 1250,
"createdAt": ISODate("2026-01-10T12:00:00.000Z"),
"updatedAt": ISODate("2026-01-20T15:30:00.000Z"),
"__v": 0
}Indexes:
_id- Automatic (Primary Key)name- Für alphabetische Sortierung (optional)
Aktuell: Keine Beziehungen zwischen Collections (Flat Structure)
Zukünftig möglich:
- User-Collection mit References zu Transactions/SavingsGoals
- Category-Collection für Transaktionen
Alle States leben in der Dashboard-Komponente und werden via Props an Child-Components weitergegeben.
Dashboard (State Owner)
│
├─► TransactionForm (Props: onAdd, savingsGoals)
│
├─► TransactionList (Props: transactions, onDelete)
│
├─► SavingsGoalForm (Props: onAdd)
│
└─► SavingsGoalCard (Props: goal, onUpdate, onDelete)
Vorteile:
- Einfache Implementierung
- Keine externe Library nötig
- Single Source of Truth
Nachteile:
- Prop Drilling bei tiefer Hierarchie
- Re-renders der gesamten Dashboard-Component
Beispiel: Transaktion hinzufügen
1. User gibt Daten im TransactionForm ein
2. User klickt "Hinzufügen"
3. onAdd() wird aufgerufen (Prop-Function)
↓
4. Dashboard.addTransaction() wird ausgeführt
↓
5. POST-Request an API
↓
6. fetchTransactions() lädt neue Daten
↓
7. setTransactions() aktualisiert State
↓
8. React re-rendert Dashboard + Children
↓
9. Neue Transaktion erscheint in UI
Die App verwendet Fetch API (nativ) statt Axios, obwohl Axios in package.json ist.
Aktuelle Implementierung:
const res = await fetch(`${API_URL}/transactions`);
const data = await res.json();Mit Axios wäre es:
const { data } = await axios.get(`${API_URL}/transactions`);Aktuell:
try {
const res = await fetch(`${API_URL}/transactions`);
const data = await res.json();
setTransactions(data);
} catch (error) {
console.error('Fehler beim Laden:', error);
}Verbesserung möglich:
try {
const res = await fetch(`${API_URL}/transactions`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
setTransactions(data);
} catch (error) {
console.error('Fehler:', error);
// User-Feedback (Toast/Alert)
}Frontend Backend Database
│ │ │
├─ POST /transactions ─►│ │
│ ├─ Validate ─────────┤
│ │ │
│ ├─ Save ────────────►│
│ │ │
│◄─────── 201 ──────────┤◄─── Document ──────┤
│ │ │
├─ GET /transactions ──►│ │
│ ├─ Query ───────────►│
│ │ │
│◄────── Data ──────────┤◄──── Results ──────┤
Farbpalette:
/* Primärfarben */
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--secondary-gradient: linear-gradient(135deg, #6366f1, #8b5cf6);
/* Einnahmen */
--income-bg: #d1fae5;
--income-text: #059669;
--income-border: #10b981;
/* Ausgaben */
--expense-bg: #fee2e2;
--expense-text: #dc2626;
--expense-border: #ef4444;
/* Saldo */
--balance-bg: #dbeafe;
--balance-text: #2563eb;
--balance-border: #3b82f6;Typography:
- Font: System-Font-Stack (San Francisco, Segoe UI, etc.)
- Sizes: 0.75rem - 2.5rem
- Weights: 500, 600, 700, 800
Inline-Styles:
const styles = {
dashboard: {
padding: '40px',
maxWidth: '1400px',
margin: '0 auto',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
minHeight: '100vh'
},
// ... weitere Styles
};
// Verwendung:
<div style={styles.dashboard}>...</div>Vorteile:
- Component-scoped (kein CSS-Clash)
- JavaScript-Power (Conditionals, Variables)
- Keine separate CSS-Datei nötig
Nachteile:
- Keine Pseudo-Klassen (:hover inline nicht möglich)
- Performance bei vielen Styles
- Keine Media Queries inline
Grid-Layout:
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))'Auto-responsive ohne Media Queries!
Breakpoints (implizit):
- Mobile: < 600px (Single Column)
- Tablet: 600-1200px (2 Columns)
- Desktop: > 1200px (3 Columns)
const lastTenTransactions = [...transactions]
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
.slice(0, 10);Effekt: Reduziert Rendering-Last und DOM-Nodes
const filteredTransactions = lastTenTransactions.filter(/* ... */);Effekt: Keine zusätzlichen API-Calls, instant Feedback
{filteredTransactions.length === 0 ? (
<p>Keine Transaktionen</p>
) : (
<TransactionList ... />
)}Effekt: Rendert nur wenn nötig
const TransactionList = React.memo(({ transactions, onDelete }) => {
// Component Code
});Effekt: Verhindert Re-Renders wenn Props gleich bleiben
const deleteTransaction = useCallback(async (id) => {
// Code
}, []);Effekt: Stabile Funktions-Referenz
const filteredTransactions = useMemo(() => {
return lastTenTransactions.filter(/* ... */);
}, [lastTenTransactions, searchTerm, filterType]);Effekt: Cached Berechnungen
Für große Listen: react-window oder react-virtualized
const debouncedSearch = useDebounce(searchTerm, 300);Effekt: Reduziert Filter-Operationen
Keine Authentication:
- Alle Daten sind öffentlich
- Keine User-Trennung
Keine Input-Sanitization:
- XSS-Anfällig bei Eingaben
// JWT-basiert
const token = jwt.sign({ userId }, SECRET, { expiresIn: '7d' });// Backend mit express-validator
body('text').trim().isLength({ min: 1 }).escape()const rateLimit = require('express-rate-limit');
app.use('/api', rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));- Nur verschlüsselte Verbindungen
- Certificates mit Let's Encrypt
- Niemals
.envin Git committen - Secrets in Deployment-Plattform verwalten
Begründung:
- App-Größe rechtfertigt Redux nicht
- Lift State Up reicht völlig aus
- Weniger Boilerplate-Code
- Einfacher zu verstehen für Anfänger
Begründung:
- Schnelle Entwicklung
- Keine CSS-Namenskonflikte
- Component-scoped Styling
- Einfacher zu maintainen für kleine App
Begründung:
- Flexible Schema (NoSQL)
- Schnelle Entwicklung
- Atlas bietet Free Tier
- Gute Node.js Integration
Begründung:
- Nativ in Browsern
- Keine zusätzliche Dependency
- Ausreichend für einfache Requests
Aber: Axios ist in package.json → Wurde ursprünglich geplant, dann aber nicht genutzt
Empfohlene Test-Pyramide:
┌──────────┐
│ E2E │ (Cypress, Playwright)
├──────────┤
│Integration│ (React Testing Library)
├──────────┤
│ Unit │ (Jest)
└──────────┘
test('calculateSummary berechnet korrekt', () => {
const txns = [
{ amount: 100, type: 'income' },
{ amount: 50, type: 'expense' }
];
const result = calculateSummary(txns);
expect(result.balance).toBe(50);
});test('TransactionForm submittet Daten', () => {
const mockAdd = jest.fn();
render(<TransactionForm onAdd={mockAdd} />);
fireEvent.change(screen.getByPlaceholderText('Beschreibung'), {
target: { value: 'Test' }
});
fireEvent.click(screen.getByText('Hinzufügen'));
expect(mockAdd).toHaveBeenCalledWith({
text: 'Test',
amount: expect.any(Number),
type: 'expense'
});
});- Users: Single-User (keine Multi-Tenancy)
- Data: Unbegrenzt in MongoDB, aber UI zeigt nur 10 Transactions
- Traffic: Kein Load Balancing
Load Balancer
│
├─► Backend Instance 1
├─► Backend Instance 2
└─► Backend Instance 3
│
└─► MongoDB (Replica Set)
- Sharding nach User-ID
- Für Millionen von Transaktionen
// Redis für häufige Queries
const cachedTransactions = await redis.get(`user:${userId}:transactions`);Diese technische Dokumentation wird regelmäßig aktualisiert! 🔄