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:

Loading diagram...

Key Components

  1. PrismaService: Extends PrismaClient with lifecycle management
  2. BaseRepository: Abstract base class for repository pattern
  3. Repository Decorator: Simplifies repository creation
  4. 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:

  1. Metadata Storage: Stores repository configuration in class metadata
  2. Model Association: Links the repository to a specific Prisma model
  3. Dependency Injection: Enables automatic injection using @InjectRepository()
  4. 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:

  1. Decorator Registration: @Repository stores model name in metadata
  2. Type Resolution: Framework reads parameter type (UserRepository)
  3. Metadata Lookup: Finds repository metadata for that type
  4. Instance Creation: Creates repository instance with PrismaService
  5. 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:

  1. Automatic Injection: No need to manually provide repositories
  2. Type Safety: TypeScript knows which model the repository manages
  3. Consistency: Ensures all repositories follow the same pattern
  4. Metadata: Framework can introspect repository configuration
  5. Simplified Setup: Less boilerplate code

Best Practices:

  1. Match model names: Repository model name must match Prisma schema
  2. Extend BaseRepository: Get CRUD methods for free
  3. Add custom methods: Extend with model-specific queries
  4. Use @InjectRepository: For automatic dependency injection
  5. 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

  1. Use repositories: Create repository classes for each model to encapsulate database logic.

  2. Type safety: Use TypeScript interfaces that match your Prisma models.

  3. Transactions: Use transactions for operations that must be atomic.

  4. Indexes: Add appropriate indexes in your Prisma schema for better performance.

  5. Migrations: Always use migrations instead of manually changing the database.

  6. Connection pooling: Configure connection pooling in production.

What's Next?

  • Learn about Cache for caching database queries
  • Explore Config for database configuration
  • Check out Auth for user authentication