Overview
This guide explores the advanced technical concepts and patterns implemented in Your Finance App. Understanding these fundamentals will help you extend functionality and maintain code quality.
TypeScript Patterns
Type Safety with Prisma
The app leverages Prisma’s generated types for end-to-end type safety:
import { Prisma , Transaction } from '@prisma/client' ;
// Type-safe where clause
const where : Prisma . TransactionWhereInput = {
userId ,
deletedAt: null ,
type: 'INCOME'
};
Prisma generates TypeScript types automatically from your schema, ensuring database queries are type-safe at compile time.
Utility Types
The codebase uses TypeScript utility types for flexibility:
// Partial - Make all fields optional for updates
type UpdateUserDto = Partial < CreateUserDto >;
// Pick - Select specific fields
type LoginDto = Pick < User , 'email' | 'password' >;
// Omit - Exclude sensitive fields
type UserResponse = Omit < User , 'password' >;
See how generics enable reusable pagination:
apps/backend/src/common/dto/pagination.dto.ts
export interface PaginatedResult < T > {
data : T [];
total : number ;
page : number ;
limit : number ;
totalPages : number ;
}
function createPaginatedResponse < T >(
data : T [],
total : number ,
page : number ,
limit : number
) : PaginatedResult < T > {
return {
data ,
total ,
page ,
limit ,
totalPages: Math . ceil ( total / limit )
};
}
NestJS Decorators
Understanding Decorators
Decorators are functions that modify classes, methods, or parameters. They’re central to NestJS’s declarative style.
Class Decorators
@ Injectable () // Marks class for dependency injection
export class TransactionsService {}
@ Controller ( 'transactions' ) // Defines route prefix
export class TransactionsController {}
@ Module ({ // Declares a module
providers: [ TransactionsService ],
controllers: [ TransactionsController ]
})
export class TransactionsModule {}
Method Decorators
@ Post ( 'register' ) // HTTP method + route
@ HttpCode ( 201 ) // Response status code
@ UseGuards ( JwtAuthGuard ) // Apply authentication
async create (@ Body () dto : CreateTransactionDto ) {
return this . service . create ( dto );
}
Parameter Decorators
apps/backend/src/transactions/transactions.controller.ts
@ Get ()
findAll (
@ Query () query : QueryTransactionDto , // Extract query params
@ CurrentUser () user : UserPayload , // Custom decorator
@ Headers ( 'authorization' ) auth : string // Extract header
) {
return this . service . findAll ( query , user . id );
}
Parameter decorators extract and validate data from the HTTP request before it reaches your handler.
Custom Decorators
Create your own decorators for reusable logic:
import { createParamDecorator , ExecutionContext } from '@nestjs/common' ;
export const CurrentUser = createParamDecorator (
( data : unknown , ctx : ExecutionContext ) => {
const request = ctx . switchToHttp (). getRequest ();
return request . user ; // Added by JWT strategy
},
);
// Usage
@ Get ( 'profile' )
getProfile (@ CurrentUser () user : UserPayload ) {
return { id: user . id , email: user . email };
}
Dependency Injection
The Problem DI Solves
// ❌ Without DI - Tight coupling
class TransactionsService {
private prisma = new PrismaService (); // Hard-coded dependency
// Problems: Hard to test, inflexible, multiple instances
}
// ✅ With DI - Loose coupling
@ Injectable ()
class TransactionsService {
constructor ( private prisma : PrismaService ) {}
// Benefits: Testable, flexible, singleton managed by NestJS
}
How It Works
Define a Provider
Mark the class as injectable: @ Injectable ()
export class PrismaService extends PrismaClient {}
Register in Module
Add to module’s providers array: @ Module ({
providers: [ PrismaService ],
exports: [ PrismaService ] // Make available to other modules
})
export class PrismaModule {}
Inject Where Needed
Request in constructor: @ Injectable ()
export class AuthService {
constructor ( private prisma : PrismaService ) {}
// NestJS automatically injects PrismaService
}
Provider Scopes
@ Injectable ({ scope: Scope . DEFAULT }) // Singleton (one instance for app)
@ Injectable ({ scope: Scope . REQUEST }) // New instance per HTTP request
@ Injectable ({ scope: Scope . TRANSIENT }) // New instance every injection
REQUEST scope can impact performance. Use DEFAULT scope unless you need request-specific state.
Prisma ORM Concepts
Generated Types
Prisma generates rich TypeScript types from your schema:
model Transaction {
id String @id @default ( uuid ())
type String
amount Decimal @db.Decimal ( 15 , 2 )
description String ?
categoryId String ?
category Category ? @relation ( fields : [ categoryId ], references : [ id ] )
}
Generates:
// Use generated Prisma types
type TransactionIncludeCategory = Prisma . TransactionGetPayload <{
include : { category : true }
}>;
Query Building
apps/backend/src/transactions/transactions.service.ts
const where : Prisma . TransactionWhereInput = {
userId ,
deletedAt: null
};
if ( type ) where . type = type ;
if ( categoryId ) where . categoryId = categoryId ;
if ( search ) {
where . OR = [
{ description: { contains: search , mode: 'insensitive' } },
{ category: { name: { contains: search , mode: 'insensitive' } } }
];
}
const transactions = await this . prisma . transaction . findMany ({
where ,
include: { category: true , account: true },
orderBy: { date: 'desc' }
});
Preventing N+1 Queries
// ❌ Bad - N+1 problem (1 + N queries)
const users = await prisma . user . findMany ();
for ( const user of users ) {
user . transactions = await prisma . transaction . findMany ({
where: { userId: user . id }
});
}
// ✅ Good - Single query with JOIN
const users = await prisma . user . findMany ({
include: { transactions: true }
});
Database Transactions
Use $transaction for atomic operations:
apps/backend/src/transactions/transactions.service.ts
await this . prisma . $transaction ( async ( tx ) => {
// Create transaction record
const newTransaction = await tx . transaction . create ({
data: { ... transactionData }
});
// Update account balance atomically
const operation = type === 'INCOME' ? 'increment' : 'decrement' ;
await tx . account . update ({
where: { id: accountId },
data: { balance: { [operation]: amount } }
});
return newTransaction ;
});
Prisma transactions ensure all operations succeed or fail together, maintaining data consistency.
Validation with class-validator
DTO Validation
apps/backend/src/transactions/dto/create-transaction.dto.ts
import { IsString , IsEnum , IsNumber , Min , MaxLength , IsDateString } from 'class-validator' ;
export class CreateTransactionDto {
@ IsEnum ([ 'INCOME' , 'EXPENSE' , 'TRANSFER' ], {
message: 'Type must be INCOME, EXPENSE or TRANSFER'
})
type : string ;
@ IsNumber ({ maxDecimalPlaces: 2 })
@ Min ( 0.01 , { message: 'Amount must be greater than 0' })
amount : number ;
@ IsString ()
@ MaxLength ( 500 )
@ IsOptional ()
description ?: string ;
@ IsDateString ()
@ IsOptional ()
date ?: string ;
}
Global Validation Pipe
Configured in main.ts:
app . useGlobalPipes (
new ValidationPipe ({
whitelist: true , // Strip non-whitelisted properties
forbidNonWhitelisted: true , // Throw error on extra properties
transform: true , // Auto-transform to DTO types
transformOptions: {
enableImplicitConversion: true
}
})
);
Exception Handling
Built-in HTTP Exceptions
import {
NotFoundException ,
BadRequestException ,
UnauthorizedException ,
ForbiddenException
} from '@nestjs/common' ;
if ( ! user ) {
throw new NotFoundException ( 'User not found' );
}
if ( user . id !== requestUserId ) {
throw new ForbiddenException ( 'Access denied' );
}
Custom Exception Filters
The app includes a Prisma exception filter that translates database errors:
apps/backend/src/common/filters/prisma-exception.filter.ts
@ Catch ( Prisma . PrismaClientKnownRequestError )
export class PrismaExceptionFilter extends BaseExceptionFilter {
catch ( exception : PrismaClientKnownRequestError , host : ArgumentsHost ) {
const ctx = host . switchToHttp ();
const response = ctx . getResponse ();
switch ( exception . code ) {
case 'P2002' : // Unique constraint violation
return response . status ( 409 ). json ({
statusCode: 409 ,
message: 'This record already exists'
});
case 'P2025' : // Record not found
return response . status ( 404 ). json ({
statusCode: 404 ,
message: 'Record not found'
});
}
}
}
Error Code Reference Complete list of Prisma error codes
NestJS Exception Filters Official documentation on exception filters
Async/Await Patterns
Promise.all for Parallel Operations
apps/backend/src/transactions/transactions.service.ts
// Fetch data and count in parallel
const [ data , total ] = await Promise . all ([
this . prisma . transaction . findMany ({ where , skip , take: limit }),
this . prisma . transaction . count ({ where })
]);
Error Handling
async function getUser ( id : string ) {
try {
const user = await this . prisma . user . findUnique ({ where: { id } });
if ( ! user ) {
throw new NotFoundException ( 'User not found' );
}
return user ;
} catch ( error ) {
this . logger . error ( 'Error fetching user' , error );
throw error ;
}
}
Authentication Strategy Pattern
The JWT strategy validates tokens and loads user data:
apps/backend/src/auth/strategies/jwt.strategy.ts
@ Injectable ()
export class JwtStrategy extends PassportStrategy ( Strategy ) {
constructor ( private prisma : PrismaService ) {
super ({
jwtFromRequest: ExtractJwt . fromAuthHeaderAsBearerToken (),
ignoreExpiration: false ,
secretOrKey: process . env . JWT_SECRET
});
}
async validate ( payload : JwtPayload ) {
const user = await this . prisma . user . findUnique ({
where: { id: payload . sub }
});
if ( ! user ) {
throw new UnauthorizedException ( 'User not found' );
}
// Return user object attached to request
return {
id: user . id ,
email: user . email ,
role: user . role
};
}
}
The validate method’s return value is attached to request.user and accessible via the @CurrentUser() decorator.
Design Patterns
Repository Pattern
Prisma acts as the repository layer:
@ Injectable ()
export class TransactionsService {
constructor ( private prisma : PrismaService ) {} // Repository
async findAll () {
return this . prisma . transaction . findMany ();
}
}
DTO Pattern
Separate API representation from domain models:
// What the API receives
class CreateTransactionDto {
amount : number ;
type : string ;
}
// Domain model from database
type Transaction = {
id : string ;
amount : Decimal ;
type : TransactionType ;
createdAt : Date ;
}
Service Layer Pattern
Controllers handle HTTP, services handle business logic:
// Controller - HTTP concerns
@ Controller ( 'transactions' )
export class TransactionsController {
constructor ( private service : TransactionsService ) {}
@ Post ()
create (@ Body () dto : CreateTransactionDto , @ CurrentUser () user ) {
return this . service . create ( dto , user . id );
}
}
// Service - Business logic
@ Injectable ()
export class TransactionsService {
async create ( dto : CreateTransactionDto , userId : string ) {
// Validate account exists
// Check sufficient funds
// Create transaction
// Update account balance
}
}
Best Practices Code organization and quality standards
Deployment Production deployment strategies