Prisma Package

npm downloads

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 PatternBaseRepository<T> for consistent data access with common CRUD methods
  • Lifecycle Management — Connect on onModuleInit, disconnect on onModuleDestroy — 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 typeCorrect 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

  1. PrismaModule — Registers PrismaService; forRoot(options) for custom connection strings
  2. PrismaService — Injectable extension of PrismaClient with onModuleInit/onModuleDestroy hooks
  3. BaseRepository — Abstract base class with findMany, findOne, create, update, delete, count
  4. @Repository — Class decorator that links the repository to a Prisma model and implies @Injectable()
  5. @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:

MethodDescription
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:

  1. Stores the model name in metadata (used by @InjectRepository)
  2. 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 CodeError message
P2002Unique constraint violation on fields: <fields>
P2025Record not found
P2003Foreign 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

  1. One repository per model — Extend BaseRepository<YourModel> and add custom query methods
  2. Use @Repository — Not @Injectable(). One decorator, not two
  3. Use @Service() — For all business-logic classes that are not repositories
  4. Type your models — Define TypeScript interfaces that match Prisma model shapes
  5. Use transactions — For any multi-table write that must be atomic
  6. Add indexes — For frequently filtered fields in your Prisma schema
  7. Use migrations — Never modify the database manually; always use prisma migrate

What's Next?

  • Learn about TypeORM for the entity-based alternative ORM
  • Explore Cache for caching database queries
  • Check out Config for database configuration management