Files
personal-finance/backend-api/ARCHITECTURE.md
Alexander Zinn cd93dcbfd2 Add backend API for personal finance management application
- Introduced a comprehensive backend API using TypeScript, Fastify, and PostgreSQL.
- Added essential files including architecture documentation, environment configuration, and Docker setup.
- Implemented RESTful routes for managing assets, liabilities, clients, invoices, and cashflow.
- Established a robust database schema with Prisma for data management.
- Integrated middleware for authentication and error handling.
- Created service and repository layers to adhere to SOLID principles and clean architecture.
- Included example environment variables for development, staging, and production setups.
2025-12-07 12:59:09 -05:00

9.3 KiB

Personal Finances API - Architecture Documentation

Overview

This backend API is built following SOLID principles and clean architecture patterns using TypeScript, Fastify, and PostgreSQL.

SOLID Principles Implementation

1. Single Responsibility Principle (SRP)

Each class has one well-defined responsibility:

  • Controllers - Handle HTTP requests/responses only
  • Services - Contain business logic only
  • Repositories - Handle data access only
  • Middleware - Handle cross-cutting concerns (auth, errors)

Example:

// AssetService - ONLY handles asset business logic
export class AssetService {
  async create(userId: string, data: CreateAssetDTO): Promise<Asset>
  async update(id: string, userId: string, data: UpdateAssetDTO): Promise<Asset>
  // ...
}

// AssetRepository - ONLY handles database operations
export class AssetRepository {
  async findById(id: string): Promise<Asset | null>
  async create(data: Prisma.AssetCreateInput): Promise<Asset>
  // ...
}

2. Open/Closed Principle (OCP)

The system is open for extension but closed for modification:

  • Custom Error Classes - Extend AppError base class for new error types
  • Repository Interfaces - Implement IRepository<T> for new entities
  • Service Pattern - Add new services without modifying existing ones

Example:

// Extensible error hierarchy
export abstract class AppError extends Error {
  abstract statusCode: number;
}

export class NotFoundError extends AppError {
  statusCode = 404;
}

export class ValidationError extends AppError {
  statusCode = 400;
}

3. Liskov Substitution Principle (LSP)

Derived classes can substitute their base classes:

// Base interface
export interface IRepository<T> {
  findById(id: string): Promise<T | null>;
  create(data: Partial<T>): Promise<T>;
}

// Specialized interface extends base without breaking it
export interface IUserScopedRepository<T> extends Omit<IRepository<T>, 'findAll'> {
  findAllByUser(userId: string): Promise<T[]>;
}

4. Interface Segregation Principle (ISP)

Clients depend only on interfaces they use:

  • IRepository<T> - Base CRUD operations
  • IUserScopedRepository<T> - User-scoped operations
  • Specific methods in services (e.g., getTotalValue() in AssetService)

5. Dependency Inversion Principle (DIP)

High-level modules depend on abstractions:

// Service depends on repository abstraction, not concrete implementation
export class AuthService {
  constructor(private userRepository: UserRepository) {}
  // UserRepository implements IRepository<User>
}

// Singleton database connection
class DatabaseConnection {
  private static instance: PrismaClient;
  public static getInstance(): PrismaClient {
    // ...
  }
}

Architecture Layers

1. Presentation Layer (Controllers & Routes)

  • Location: src/controllers/, src/routes/
  • Purpose: Handle HTTP requests/responses
  • Responsibilities:
    • Parse request parameters
    • Validate input schemas (using Zod)
    • Call service layer
    • Format responses
export class AssetController {
  async create(request: FastifyRequest, reply: FastifyReply) {
    const userId = getUserId(request);
    const data = createAssetSchema.parse(request.body); // Validation
    const asset = await this.assetService.create(userId, data); // Business logic
    return reply.status(201).send({asset}); // Response
  }
}

2. Business Logic Layer (Services)

  • Location: src/services/
  • Purpose: Implement business rules
  • Responsibilities:
    • Validate business rules
    • Coordinate between repositories
    • Perform calculations
    • Enforce authorization
export class InvoiceService {
  async create(userId: string, data: CreateInvoiceDTO): Promise<Invoice> {
    this.validateInvoiceData(data); // Business rule
    const invoiceNumber = await this.generateInvoiceNumber(userId); // Logic
    const subtotal = this.calculateSubtotal(data.lineItems); // Calculation
    return this.invoiceRepository.create({...}); // Data access
  }
}

3. Data Access Layer (Repositories)

  • Location: src/repositories/
  • Purpose: Abstract database operations
  • Responsibilities:
    • CRUD operations
    • Query composition
    • Data mapping
export class AssetRepository implements IUserScopedRepository<Asset> {
  async findAllByUser(userId: string): Promise<Asset[]> {
    return prisma.asset.findMany({
      where: {userId},
      orderBy: {createdAt: 'desc'},
    });
  }
}

4. Cross-Cutting Concerns (Middleware & Utils)

  • Location: src/middleware/, src/utils/
  • Purpose: Handle common functionality
  • Components:
    • Authentication middleware
    • Error handling
    • Password utilities
    • Custom errors

Data Flow

HTTP Request
    ↓
Route (Fastify)
    ↓
Controller
    ├→ Validate Input (Zod)
    ├→ Extract User ID (Middleware)
    └→ Call Service
        ↓
Service
    ├→ Validate Business Rules
    ├→ Perform Calculations
    └→ Call Repository
        ↓
Repository
    ├→ Compose Query
    └→ Execute via Prisma
        ↓
Database (PostgreSQL)

Database Schema

Entity Relationships

User
├── Assets (1:N)
├── Liabilities (1:N)
├── NetWorthSnapshots (1:N)
├── Clients (1:N)
│   └── Invoices (1:N)
│       └── InvoiceLineItems (1:N)
├── IncomeSources (1:N)
├── Expenses (1:N)
├── Transactions (1:N)
└── DebtCategories (1:N)
    └── DebtAccounts (1:N)
        └── DebtPayments (1:N)

Key Design Decisions

  1. Cascade Deletes: User deletion cascades to all related data
  2. UUID Primary Keys: For security and distributed systems
  3. Timestamps: All entities track creation/update times
  4. Enums: Type-safe status and category fields
  5. Composite Indexes: Optimized queries on user_id + other fields

Security Features

1. Authentication

  • JWT tokens with configurable expiration
  • Secure password hashing (bcrypt with 10 rounds)
  • Password complexity requirements

2. Authorization

  • User-scoped data access
  • Repository methods verify ownership
  • Middleware extracts authenticated user

3. Input Validation

  • Zod schemas for runtime validation
  • Type-safe request/response handling
  • SQL injection prevention (Prisma ORM)

4. Error Handling

  • Custom error classes
  • No sensitive information in error messages
  • Proper HTTP status codes

API Design

RESTful Conventions

  • GET /api/resources - List all
  • GET /api/resources/:id - Get one
  • POST /api/resources - Create
  • PUT /api/resources/:id - Update (full)
  • PATCH /api/resources/:id - Update (partial)
  • DELETE /api/resources/:id - Delete

Response Format

{
  "resource": { /* data */ },
  // or
  "resources": [ /* array */ ],
  // or on error
  "error": "ErrorType",
  "message": "Human-readable message"
}

HTTP Status Codes

  • 200 OK - Successful GET/PUT/PATCH
  • 201 Created - Successful POST
  • 204 No Content - Successful DELETE
  • 400 Bad Request - Validation error
  • 401 Unauthorized - Authentication required
  • 403 Forbidden - Insufficient permissions
  • 404 Not Found - Resource not found
  • 409 Conflict - Duplicate resource
  • 500 Internal Server Error - Server error

Testing Strategy

Unit Tests

  • Test services in isolation
  • Mock repository dependencies
  • Test business logic thoroughly

Integration Tests

  • Test API endpoints
  • Use test database
  • Verify request/response flow

E2E Tests

  • Test complete user flows
  • Verify authentication
  • Test error scenarios

Performance Considerations

Database

  • Indexes on frequently queried fields
  • Connection pooling (Prisma)
  • Efficient query composition

Caching

  • JWT tokens cached in client
  • Consider Redis for session management
  • Database query result caching

Scalability

  • Stateless API (horizontal scaling)
  • Database migrations for schema changes
  • Environment-based configuration

Development Workflow

  1. Add New Feature:

    • Define Prisma schema
    • Run migration
    • Create repository interface
    • Implement repository
    • Create service with business logic
    • Add controller
    • Define routes
    • Add tests
  2. Modify Existing Feature:

    • Update service layer (business logic)
    • Update repository if needed
    • Update schema if needed
    • Update tests

Best Practices

Code Organization

One class per file Group related files in directories Use barrel exports (index.ts) Consistent naming conventions

Error Handling

Use custom error classes Validate at boundaries Log errors appropriately Return user-friendly messages

Security

Never log sensitive data Validate all inputs Use parameterized queries (Prisma) Implement rate limiting Keep dependencies updated

Documentation

JSDoc comments for public APIs README for setup instructions API documentation (Swagger) Architecture documentation (this file)

Future Enhancements

  • Rate limiting middleware
  • Request logging
  • Metrics and monitoring
  • Database query optimization
  • Caching layer
  • WebSocket support for real-time updates
  • Background job processing
  • Email notifications
  • Data export functionality
  • Multi-tenancy support