Prisma Package
The @hazeljs/prisma package provides seamless Prisma ORM integration for HazelJS. It includes an injectable PrismaService, a BaseRepository with full CRUD, automatic connection lifecycle, and @Repository / @InjectRepository decorators for clean DI.
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 —
BaseRepository<T>for consistent data access with common CRUD methods - Lifecycle Management — Connect on
onModuleInit, disconnect ononModuleDestroy— no manual wiring - Error Handling — Built-in mapping of Prisma error codes (P2002, P2025, P2003) to readable messages
- Type Safety — Full TypeScript support with Prisma-generated types end-to-end
Decorator Convention
| Class type | Correct decorator |
|---|---|
Repository (extends BaseRepository) | @Repository({ model: '...' }) — implies @Injectable() |
| Service (business logic) | @Service() |
| Controller | @Controller(...) |
@Repository marks the class as injectable automatically. You do not need @Injectable() alongside it.
Architecture
The package extends Prisma's client and integrates with HazelJS dependency injection:
graph TD A["Controllers & Services"] --> B["BaseRepository<br/>(CRUD & Custom Methods)"] B --> C["PrismaService<br/>(Extends PrismaClient)"] C --> D["Prisma Client<br/>(Generated)"] D --> E["Database<br/>(PostgreSQL, MySQL, etc)"] F["@Repository decorator"] --> B G["@Service decorator"] --> A style A fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff style B fill:#6366f1,stroke:#818cf8,stroke-width:2px,color:#fff style C fill:#8b5cf6,stroke:#a78bfa,stroke-width:2px,color:#fff style D fill:#f59e0b,stroke:#fbbf24,stroke-width:2px,color:#fff style E fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff style F fill:#ec4899,stroke:#f472b6,stroke-width:2px,color:#fff style G fill:#ec4899,stroke:#f472b6,stroke-width:2px,color:#fff
Key Components
- PrismaModule — Registers
PrismaService;forRoot(options)for custom connection strings - PrismaService — Injectable extension of
PrismaClientwithonModuleInit/onModuleDestroyhooks - BaseRepository — Abstract base class with
findMany,findOne,create,update,delete,count - @Repository — Class decorator that links the repository to a Prisma model and implies
@Injectable() - @InjectRepository — Parameter decorator to inject repository instances into services
Advantages
1. Type Safety
Leverage Prisma's generated types for end-to-end type safety from database schema to API response.
2. Repository Pattern
Consistent data access with BaseRepository<T>. Add custom methods per model; share the same CRUD baseline everywhere.
3. Lifecycle Management
PrismaService connects and disconnects automatically with the module — no manual $connect() / $disconnect() calls.
4. @Repository Implies @Injectable
@Repository does everything: links the class to a Prisma model, stores metadata for @InjectRepository, and registers the class as injectable. One decorator, not two.
5. Error Handling
BaseRepository maps Prisma error codes to readable messages: P2002 → unique constraint, P2025 → not found, P2003 → foreign key violation.
6. Production Ready
Includes query logging, Prisma middleware support, connection pooling, and full transaction API.
Installation
npm install @hazeljs/prisma @prisma/client
npm install -D prisma
Quick Start
Setup Prisma
npx prisma init
Define your schema in prisma/schema.prisma:
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 client and run the first migration:
npx prisma generate
npx prisma migrate dev --name init
Register Prisma Module
import { HazelModule } from '@hazeljs/core';
import { PrismaModule } from '@hazeljs/prisma';
@HazelModule({
imports: [PrismaModule],
})
export class AppModule {}
Prisma Service
Inject PrismaService directly when you need full Prisma query flexibility without a repository:
import { Service } from '@hazeljs/core';
import { PrismaService } from '@hazeljs/prisma';
@Service()
export class UserService {
constructor(private readonly prisma: PrismaService) {}
async findAll() {
return this.prisma.user.findMany();
}
async findOne(id: number) {
return this.prisma.user.findUnique({ where: { id } });
}
async create(data: { email: string; name?: string }) {
return this.prisma.user.create({ data });
}
async update(id: number, data: { email?: string; name?: string }) {
return this.prisma.user.update({ where: { id }, data });
}
async delete(id: number) {
return this.prisma.user.delete({ where: { id } });
}
}
Base Repository
Use BaseRepository<T> for the standard repository pattern. Pass the Prisma model name (lowercase, singular) to super():
import { Repository, BaseRepository, PrismaService } from '@hazeljs/prisma';
interface User {
id: number;
email: string;
name: string | null;
createdAt: Date;
updatedAt: Date;
}
// @Repository implies @Injectable() — no need for both
@Repository({ model: 'user' })
export class UserRepository extends BaseRepository<User> {
constructor(prisma: PrismaService) {
super(prisma, 'user');
}
async findByEmail(email: string) {
return this.prisma.user.findUnique({ where: { email } });
}
async findManyWithPosts() {
return this.prisma.user.findMany({ include: { posts: true } });
}
}
Available BaseRepository<T> methods:
| Method | Description |
|---|---|
findMany() | Return all records |
findOne(where) | Find one by unique field or null |
create(data) | Create and return new record |
update(where, data) | Update by unique field |
delete(where) | Delete by unique field |
count(args?) | Count matching records |
@Repository Decorator
@Repository is a class decorator that:
- Stores the model name in metadata (used by
@InjectRepository) - Implicitly registers the class as injectable —
@Injectable()is not needed
// ✅ Correct — @Injectable() is not needed
@Repository({ model: 'user' })
export class UserRepository extends BaseRepository<User> {
constructor(prisma: PrismaService) {
super(prisma, 'user');
}
}
// ❌ Redundant — @Injectable() is already implied by @Repository
@Injectable()
@Repository({ model: 'user' })
export class UserRepository extends BaseRepository<User> { /* ... */ }
Shorthand with string: @Repository('user').
To use a non-singleton scope: @Repository({ model: 'user', scope: 'transient' }).
@InjectRepository
The @InjectRepository parameter decorator injects a repository instance by reading the class type from the constructor:
import { Service } from '@hazeljs/core';
import { InjectRepository } from '@hazeljs/prisma';
@Service()
export class UserService {
constructor(
@InjectRepository() private readonly userRepository: UserRepository,
) {}
async findAll() {
return this.userRepository.findMany();
}
}
Complete Example
Repository, service, and controller fully wired:
import { Service, Controller, Get, Post, Put, Delete, Param, Body } from '@hazeljs/core';
import { Repository, BaseRepository, PrismaService, InjectRepository } from '@hazeljs/prisma';
interface User { id: number; email: string; name: string | null; }
// @Repository implies @Injectable() — no separate decorator needed
@Repository({ model: 'user' })
export class UserRepository extends BaseRepository<User> {
constructor(prisma: PrismaService) {
super(prisma, 'user');
}
async findByEmail(email: string) {
return this.findOne({ email });
}
}
@Service()
export class UserService {
constructor(
@InjectRepository() private readonly userRepository: UserRepository,
) {}
async findAll() {
return this.userRepository.findMany();
}
async findOne(id: number) {
return this.userRepository.findOne({ id });
}
async create(data: Omit<User, 'id'>) {
return this.userRepository.create(data);
}
async update(id: number, data: Partial<User>) {
return this.userRepository.update({ id }, data);
}
async delete(id: number) {
return this.userRepository.delete({ id });
}
}
@Controller('users')
export class UsersController {
constructor(private readonly userService: UserService) {}
@Get()
findAll() { return this.userService.findAll(); }
@Get(':id')
findOne(@Param('id') id: string) { return this.userService.findOne(parseInt(id)); }
@Post()
create(@Body() body: { email: string; name?: string }) { return this.userService.create(body); }
@Put(':id')
update(@Param('id') id: string, @Body() body: Partial<User>) {
return this.userService.update(parseInt(id), body);
}
@Delete(':id')
delete(@Param('id') id: string) { return this.userService.delete(parseInt(id)); }
}
Transactions
Use Prisma's $transaction API for atomic multi-step operations:
Basic Transaction
import { Service } from '@hazeljs/core';
import { PrismaService } from '@hazeljs/prisma';
@Service()
export class TransferService {
constructor(private readonly prisma: PrismaService) {}
async transfer(fromId: string, toId: string, amount: number) {
return this.prisma.$transaction(async (tx) => {
await tx.account.update({
where: { id: fromId },
data: { balance: { decrement: amount } },
});
await tx.account.update({
where: { id: toId },
data: { balance: { increment: amount } },
});
return tx.transaction.create({
data: { fromId, toId, amount },
});
});
}
}
Transaction with Multiple Repositories
import { Service } from '@hazeljs/core';
import { PrismaService } from '@hazeljs/prisma';
@Service()
export class OrderService {
constructor(private readonly prisma: PrismaService) {}
async placeOrder(userId: string, items: { productId: string; quantity: number }[]) {
return this.prisma.$transaction(async (tx) => {
const order = await tx.order.create({
data: { userId, items: { create: items } },
});
for (const item of items) {
await tx.inventory.update({
where: { productId: item.productId },
data: { quantity: { decrement: item.quantity } },
});
}
return order;
});
}
}
Advanced Queries
Relations
@Repository({ model: 'user' })
export class UserRepository extends BaseRepository<User> {
constructor(prisma: PrismaService) {
super(prisma, 'user');
}
async findWithRelations(id: string) {
return this.prisma.user.findUnique({
where: { id },
include: {
posts: { where: { published: true }, orderBy: { createdAt: 'desc' } },
profile: true,
},
});
}
}
Pagination
@Repository({ model: 'post' })
export class PostRepository extends BaseRepository<Post> {
constructor(prisma: PrismaService) {
super(prisma, 'post');
}
async findPaginated(page: number, limit: number) {
const skip = (page - 1) * limit;
const [posts, total] = await Promise.all([
this.prisma.post.findMany({ skip, take: limit, orderBy: { createdAt: 'desc' } }),
this.prisma.post.count(),
]);
return {
data: posts,
meta: { page, limit, total, totalPages: Math.ceil(total / limit) },
};
}
}
Filtering
@Repository({ model: 'product' })
export class ProductRepository extends BaseRepository<Product> {
constructor(prisma: PrismaService) {
super(prisma, 'product');
}
async search(query: string, filters: {
category?: string;
minPrice?: number;
maxPrice?: number;
inStock?: boolean;
}) {
return this.prisma.product.findMany({
where: {
AND: [
{ OR: [
{ name: { contains: query, mode: 'insensitive' } },
{ description: { contains: query, mode: 'insensitive' } },
]},
filters.category ? { category: filters.category } : {},
filters.minPrice ? { price: { gte: filters.minPrice } } : {},
filters.maxPrice ? { price: { lte: filters.maxPrice } } : {},
filters.inStock ? { stock: { gt: 0 } } : {},
],
},
});
}
}
Aggregations
import { Service } from '@hazeljs/core';
import { PrismaService } from '@hazeljs/prisma';
@Service()
export class AnalyticsService {
constructor(private readonly prisma: PrismaService) {}
async getOrderStats() {
return this.prisma.order.aggregate({
_sum: { total: true },
_avg: { total: true },
_count: true,
_max: { total: true },
_min: { total: true },
});
}
async getRevenueByMonth() {
return this.prisma.$queryRaw`
SELECT DATE_TRUNC('month', "createdAt") as month,
SUM(total) as revenue,
COUNT(*) as orders
FROM "Order"
GROUP BY month
ORDER BY month DESC
`;
}
}
Error Handling
BaseRepository automatically maps Prisma error codes:
| Prisma Code | Error message |
|---|---|
P2002 | Unique constraint violation on fields: <fields> |
P2025 | Record not found |
P2003 | Foreign key constraint violation |
try {
await userRepository.create({ email: 'existing@example.com', name: 'Duplicate' });
} catch (err) {
console.error(err.message);
// => 'Unique constraint violation on fields: email'
}
Migrations
# Create a migration
npx prisma migrate dev --name add_user_role
# Apply migrations in production
npx prisma migrate deploy
# Reset database (dev only)
npx prisma migrate reset
# Regenerate client after schema changes
npx prisma generate
# Open Prisma Studio (visual DB browser)
npx prisma studio
Best Practices
- One repository per model — Extend
BaseRepository<YourModel>and add custom query methods - Use
@Repository— Not@Injectable(). One decorator, not two - Use
@Service()— For all business-logic classes that are not repositories - Type your models — Define TypeScript interfaces that match Prisma model shapes
- Use transactions — For any multi-table write that must be atomic
- Add indexes — For frequently filtered fields in your Prisma schema
- Use migrations — Never modify the database manually; always use
prisma migrate