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.
This commit is contained in:
2025-12-07 12:59:09 -05:00
parent 9d493ba82f
commit cd93dcbfd2
70 changed files with 8649 additions and 6 deletions

View File

@@ -0,0 +1,36 @@
import {FastifyRequest, FastifyReply} from 'fastify';
import {UnauthorizedError} from '../utils/errors';
/**
* Extend Fastify Request with user property
*/
declare module 'fastify' {
interface FastifyRequest {
user?: {
id: string;
email: string;
};
}
}
/**
* Authentication Middleware
* Verifies JWT token and attaches user to request
*/
export async function authenticate(request: FastifyRequest, reply: FastifyReply) {
try {
await request.jwtVerify();
} catch (err) {
throw new UnauthorizedError('Invalid or expired token');
}
}
/**
* Extract user ID from authenticated request
*/
export function getUserId(request: FastifyRequest): string {
if (!request.user || !request.user.id) {
throw new UnauthorizedError('User not authenticated');
}
return request.user.id;
}

View File

@@ -0,0 +1,64 @@
import {FastifyError, FastifyReply, FastifyRequest} from 'fastify';
import {AppError} from '../utils/errors';
import {ZodError} from 'zod';
/**
* Global Error Handler
* Implements Single Responsibility: Handles all error responses
*/
export async function errorHandler(error: FastifyError, request: FastifyRequest, reply: FastifyReply) {
// Log error
request.log.error(error);
// Handle custom app errors
if (error instanceof AppError) {
return reply.status(error.statusCode).send({
error: error.name,
message: error.message,
});
}
// Handle Zod validation errors
if (error instanceof ZodError) {
return reply.status(400).send({
error: 'ValidationError',
message: 'Invalid request data',
details: error.errors,
});
}
// Handle Fastify validation errors
if (error.validation) {
return reply.status(400).send({
error: 'ValidationError',
message: error.message,
details: error.validation,
});
}
// Handle Prisma errors
if (error.name === 'PrismaClientKnownRequestError') {
const prismaError = error as any;
if (prismaError.code === 'P2002') {
return reply.status(409).send({
error: 'ConflictError',
message: 'A record with this value already exists',
});
}
if (prismaError.code === 'P2025') {
return reply.status(404).send({
error: 'NotFoundError',
message: 'Record not found',
});
}
}
// Default server error
const statusCode = error.statusCode || 500;
const message = statusCode === 500 ? 'Internal server error' : error.message;
return reply.status(statusCode).send({
error: 'ServerError',
message,
});
}