Prisma Package
The @hazeljs/prisma package provides seamless integration with Prisma ORM for your HazelJS applications. It includes a Prisma service, base repository pattern, and automatic connection management.
Purpose
Working with databases requires managing connections, handling queries, implementing repositories, and dealing with transactions. The @hazeljs/prisma package simplifies database operations by providing:
- Prisma Integration: First-class support for Prisma ORM with automatic connection management
- Repository Pattern: Base repository class for consistent data access patterns
- Lifecycle Management: Automatic connection handling on module initialization and destruction
- Error Handling: Built-in error handling for common database errors
- Type Safety: Full TypeScript support with Prisma-generated types
Architecture
The package extends Prisma's client and integrates with HazelJS's dependency injection:
Key Components
- PrismaService: Extends PrismaClient with lifecycle management
- BaseRepository: Abstract base class for repository pattern
- Repository Decorator: Simplifies repository creation
- Error Handling: Automatic handling of Prisma-specific errors
Advantages
1. Type Safety
Leverage Prisma's generated types for end-to-end type safety from database to API.
2. Repository Pattern
Consistent data access pattern across your application with the base repository class.
3. Automatic Lifecycle
Connection management is handled automatically—no need to manually connect or disconnect.
4. Error Handling
Built-in error handling for common Prisma errors like unique constraint violations and foreign key errors.
5. Developer Experience
Simple API with dependency injection makes database operations intuitive and testable.
6. Production Ready
Includes query logging, error tracking, and proper connection pooling.
Installation
npm install @hazeljs/prisma @prisma/client
npm install -D prisma
Quick Start
Setup Prisma
Initialize Prisma in your project:
npx prisma init
This creates a prisma/schema.prisma file. Define your schema:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Generate the Prisma client:
npx prisma generate
Register Prisma Module
import { HazelModule } from '@hazeljs/core';
import { PrismaModule } from '@hazeljs/prisma';
@HazelModule({
imports: [PrismaModule],
})
export class AppModule {}
Prisma Service
The PrismaService extends PrismaClient and provides automatic connection management:
import { Injectable } from '@hazeljs/core';
import { PrismaService } from '@hazeljs/prisma';
@Injectable()
export class UserService {
constructor(private readonly prisma: PrismaService) {}
async findAll() {
return await this.prisma.user.findMany();
}
async findOne(id: number) {
return await this.prisma.user.findUnique({
where: { id },
});
}
async create(data: { email: string; name?: string }) {
return await this.prisma.user.create({
data,
});
}
async update(id: number, data: { email?: string; name?: string }) {
return await this.prisma.user.update({
where: { id },
data,
});
}
async delete(id: number) {
return await this.prisma.user.delete({
where: { id },
});
}
}
Base Repository
Use the BaseRepository for a consistent repository pattern:
import { Injectable } from '@hazeljs/core';
import { PrismaService, BaseRepository } from '@hazeljs/prisma';
interface User {
id: number;
email: string;
name: string | null;
createdAt: Date;
updatedAt: Date;
}
@Injectable()
export class UserRepository extends BaseRepository<User> {
constructor(prisma: PrismaService) {
super(prisma, 'user');
// 'user' is the Prisma model name (lowercase, singular)
// Must match your Prisma schema model name
}
// Add custom methods
async findByEmail(email: string) {
return await this.prisma.user.findUnique({
where: { email },
});
}
async findManyWithPosts() {
return await this.prisma.user.findMany({
include: {
posts: true,
},
});
}
}
@Repository Decorator
The @Repository decorator is a class decorator that simplifies repository creation and enables automatic dependency injection.
Understanding @Repository Decorator
Purpose: Marks a class as a repository and configures it for automatic injection. It stores metadata about which Prisma model the repository manages.
How it works:
- Metadata Storage: Stores repository configuration in class metadata
- Model Association: Links the repository to a specific Prisma model
- Dependency Injection: Enables automatic injection using
@InjectRepository() - Type Safety: Provides type information for the framework
Configuration Options:
interface RepositoryOptions {
model: string; // Prisma model name (e.g., 'user', 'product', 'order')
}
Example with Detailed Explanation:
import { Injectable } from '@hazeljs/core';
import { Repository, BaseRepository, PrismaService } from '@hazeljs/prisma';
interface User {
id: number;
email: string;
name: string | null;
}
// @Repository is a class decorator that marks this as a repository
@Repository({ model: 'user' })
// 'user' must match your Prisma schema model name (case-sensitive)
// This links the repository to the Prisma 'user' model
@Injectable()
export class UserRepository extends BaseRepository<User> {
constructor(prisma: PrismaService) {
// Call parent constructor with PrismaService and model name
super(prisma, 'user');
// The model name here should match @Repository decorator
}
// Custom repository methods
async findByEmail(email: string) {
// Access Prisma model through this.prisma
// TypeScript knows 'user' model exists
return await this.prisma.user.findUnique({
where: { email },
});
}
// Use base repository methods
async getAllUsers() {
// Inherited from BaseRepository
return await this.findMany();
}
}
Using @InjectRepository Decorator:
The @InjectRepository parameter decorator enables automatic repository injection:
import { Injectable } from '@hazeljs/core';
import { InjectRepository } from '@hazeljs/prisma';
@Injectable()
export class UserService {
constructor(
// @InjectRepository is a parameter decorator
// Automatically resolves the UserRepository
@InjectRepository() private userRepository: UserRepository
) {
// userRepository is automatically injected
// Framework finds it by the repository type
}
async findAll() {
// Use the injected repository
return await this.userRepository.findMany();
}
}
How Repository Injection Works:
- Decorator Registration:
@Repositorystores model name in metadata - Type Resolution: Framework reads parameter type (
UserRepository) - Metadata Lookup: Finds repository metadata for that type
- Instance Creation: Creates repository instance with
PrismaService - Dependency Injection: Injects the instance into your service
Complete Example with All Features:
// 1. Define repository with @Repository decorator
@Repository({ model: 'user' })
@Injectable()
export class UserRepository extends BaseRepository<User> {
constructor(prisma: PrismaService) {
super(prisma, 'user');
}
// Custom methods
async findByEmail(email: string) {
return await this.prisma.user.findUnique({ where: { email } });
}
async findActiveUsers() {
return await this.prisma.user.findMany({
where: { active: true },
});
}
}
// 2. Inject repository using @InjectRepository
@Injectable()
export class UserService {
constructor(
@InjectRepository() private userRepository: UserRepository
) {}
async getUserByEmail(email: string) {
// Use custom repository method
return await this.userRepository.findByEmail(email);
}
async getAllActiveUsers() {
// Use custom repository method
return await this.userRepository.findActiveUsers();
}
async createUser(data: CreateUserDto) {
// Use base repository method
return await this.userRepository.create(data);
}
}
Benefits of @Repository Decorator:
- Automatic Injection: No need to manually provide repositories
- Type Safety: TypeScript knows which model the repository manages
- Consistency: Ensures all repositories follow the same pattern
- Metadata: Framework can introspect repository configuration
- Simplified Setup: Less boilerplate code
Best Practices:
- Match model names: Repository model name must match Prisma schema
- Extend BaseRepository: Get CRUD methods for free
- Add custom methods: Extend with model-specific queries
- Use @InjectRepository: For automatic dependency injection
- Type your interfaces: Define TypeScript interfaces matching Prisma models
Complete Example
Repository Pattern
import { Injectable } from '@hazeljs/core';
import { PrismaService, BaseRepository } from '@hazeljs/prisma';
interface User {
id: number;
email: string;
name: string | null;
}
@Injectable()
export class UserRepository extends BaseRepository<User> {
constructor(prisma: PrismaService) {
super(prisma, 'user');
}
async findByEmail(email: string) {
return await this.findOne({ email });
}
async findActiveUsers() {
return await this.prisma.user.findMany({
where: {
active: true,
},
});
}
}
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
async findAll() {
return await this.userRepository.findMany();
}
async findOne(id: number) {
return await this.userRepository.findOne({ id });
}
async create(data: Omit<User, 'id'>) {
return await this.userRepository.create(data);
}
async update(id: number, data: Partial<User>) {
return await this.userRepository.update({ id }, data);
}
async delete(id: number) {
return await this.userRepository.delete({ id });
}
}
import { Controller, Get, Post, Put, Delete, Param, Body } from '@hazeljs/core';
@Controller('users')
export class UsersController {
constructor(private readonly userService: UserService) {}
@Get()
async findAll() {
return await this.userService.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string) {
return await this.userService.findOne(parseInt(id));
}
@Post()
async create(@Body() createUserDto: { email: string; name?: string }) {
return await this.userService.create(createUserDto);
}
@Put(':id')
async update(
@Param('id') id: string,
@Body() updateUserDto: { email?: string; name?: string }
) {
return await this.userService.update(parseInt(id), updateUserDto);
}
@Delete(':id')
async delete(@Param('id') id: string) {
return await this.userService.delete(parseInt(id));
}
}
Transactions
Use Prisma transactions for atomic operations:
@Injectable()
export class OrderService {
constructor(private readonly prisma: PrismaService) {}
async createOrder(orderData: any) {
return await this.prisma.$transaction(async (tx) => {
// Create order
const order = await tx.order.create({
data: orderData,
});
// Update inventory
await tx.product.updateMany({
where: {
id: { in: orderData.productIds },
},
data: {
stock: { decrement: 1 },
},
});
return order;
});
}
}
Migrations
Run migrations:
# Create migration
npx prisma migrate dev --name init
# Apply migrations
npx prisma migrate deploy
# Reset database
npx prisma migrate reset
Connection Management
The PrismaService automatically handles connection lifecycle:
- Connects on module initialization
- Disconnects on module destruction
- Logs queries and errors in development
Error Handling
The base repository includes error handling for common Prisma errors:
// Unique constraint violation
try {
await userRepository.create({ email: 'existing@example.com' });
} catch (error) {
// Handled automatically by BaseRepository
}
Best Practices
-
Use repositories: Create repository classes for each model to encapsulate database logic.
-
Type safety: Use TypeScript interfaces that match your Prisma models.
-
Transactions: Use transactions for operations that must be atomic.
-
Indexes: Add appropriate indexes in your Prisma schema for better performance.
-
Migrations: Always use migrations instead of manually changing the database.
-
Connection pooling: Configure connection pooling in production.