Skip to main content

Prisma ORM Integration

Integrate Prisma ORM with Navios for type-safe database access.

Installation

npm install prisma @prisma/client @prisma/adapter-pg
npx prisma init

Setup

1. Define Your Schema

// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model User {
id String @id @default(cuid())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

2. Create Prisma Factory

Use the @Factory decorator with an InjectionToken to create the Prisma client:

// src/database/prisma.factory.ts
import { Factory, InjectionToken } from '@navios/core'

import { PrismaPg } from '@prisma/adapter-pg'
import { PrismaClient } from '@prisma/client'

export const PrismaService = InjectionToken.create<PrismaClient>(
Symbol('PrismaClient'),
)

@Factory({
token: PrismaService,
})
export class PrismaFactory {
async create() {
const adapter = new PrismaPg({
connectionString: process.env['DATABASE_URL'],
})

const client = new PrismaClient({
adapter,
})

await client.$connect()

return client
}
}

3. With ConfigService

For better configuration management, use ConfigService:

// src/database/prisma.factory.ts
import { Factory, InjectionToken, provideConfig } from '@navios/core'
import { inject } from '@navios/di'

import { PrismaPg } from '@prisma/adapter-pg'
import { PrismaClient } from '@prisma/client'

// Define database config
interface DatabaseConfig {
database: {
url: string
logging: boolean
}
}

export const DatabaseConfigToken = provideConfig<DatabaseConfig>({
load: () => ({
database: {
url: process.env['DATABASE_URL'] || '',
logging: process.env['DATABASE_LOGGING'] === 'true',
},
}),
})

export const PrismaService = InjectionToken.create<PrismaClient>(
Symbol('PrismaClient'),
)

@Factory({
token: PrismaService,
})
export class PrismaFactory {
private config = inject(DatabaseConfigToken)

async create() {
const dbConfig = this.config.getOrThrow('database')

const adapter = new PrismaPg({
connectionString: dbConfig.url,
})

const client = new PrismaClient({
adapter,
log: dbConfig.logging ? ['query', 'info', 'warn', 'error'] : ['error'],
})

await client.$connect()

return client
}
}

4. Create Repository Services

// src/services/user.service.ts
import { NotFoundException } from '@navios/core'
import { inject, Injectable } from '@navios/di'

import { PrismaService } from '../database/prisma.factory.js'

@Injectable()
export class UserService {
private prisma = inject(PrismaService)

async findAll(params?: { skip?: number; take?: number }) {
return this.prisma.user.findMany({
skip: params?.skip,
take: params?.take,
orderBy: { createdAt: 'desc' },
})
}

async findById(id: string) {
const user = await this.prisma.user.findUnique({
where: { id },
include: { posts: true },
})

if (!user) {
throw new NotFoundException(`User ${id} not found`)
}

return user
}

async findByEmail(email: string) {
return this.prisma.user.findUnique({
where: { email },
})
}

async create(data: { email: string; name?: string }) {
return this.prisma.user.create({ data })
}

async update(id: string, data: { email?: string; name?: string }) {
await this.findById(id) // Throws if not found
return this.prisma.user.update({
where: { id },
data,
})
}

async delete(id: string) {
await this.findById(id) // Throws if not found
return this.prisma.user.delete({
where: { id },
})
}
}

5. Create Controller

// src/controllers/user.controller.ts
import { Controller, Endpoint, EndpointParams, HttpCode } from '@navios/core'
import { inject } from '@navios/di'

import {
createUser,
deleteUser,
getUser,
listUsers,
updateUser,
} from '../api/users.js'
import { UserService } from '../services/user.service.js'

@Controller()
export class UserController {
private userService = inject(UserService)

@Endpoint(listUsers)
async listUsers(params: EndpointParams<typeof listUsers>) {
return this.userService.findAll({
skip: (params.query.page - 1) * params.query.limit,
take: params.query.limit,
})
}

@Endpoint(getUser)
async getUser(params: EndpointParams<typeof getUser>) {
return this.userService.findById(params.urlParams.userId)
}

@Endpoint(createUser)
@HttpCode(201)
async createUser(params: EndpointParams<typeof createUser>) {
return this.userService.create(params.data)
}

@Endpoint(updateUser)
async updateUser(params: EndpointParams<typeof updateUser>) {
return this.userService.update(params.urlParams.userId, params.data)
}

@Endpoint(deleteUser)
@HttpCode(204)
async deleteUser(params: EndpointParams<typeof deleteUser>) {
await this.userService.delete(params.urlParams.userId)
}
}

6. Define API Endpoints

// src/api/users.ts
import { builder } from '@navios/builder'

import { z } from 'zod'

const API = builder()

const userSchema = z.object({
id: z.string(),
email: z.string(),
name: z.string().nullable(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
})

export const listUsers = API.declareEndpoint({
method: 'GET',
url: '/users',
querySchema: z.object({
page: z.coerce.number().default(1),
limit: z.coerce.number().default(10),
}),
responseSchema: z.array(userSchema),
})

export const getUser = API.declareEndpoint({
method: 'GET',
url: '/users/$userId',
responseSchema: userSchema,
})

export const createUser = API.declareEndpoint({
method: 'POST',
url: '/users',
requestSchema: z.object({
email: z.string().email(),
name: z.string().optional(),
}),
responseSchema: userSchema,
})

export const updateUser = API.declareEndpoint({
method: 'PUT',
url: '/users/$userId',
requestSchema: z.object({
email: z.string().email().optional(),
name: z.string().optional(),
}),
responseSchema: userSchema,
})

export const deleteUser = API.declareEndpoint({
method: 'DELETE',
url: '/users/$userId',
responseSchema: z.object({}),
})

Transactions

Use Prisma transactions for atomic operations:

@Injectable()
export class OrderService {
private prisma = inject(PrismaService)

async createOrder(userId: string, items: OrderItem[]) {
return this.prisma.$transaction(async (tx) => {
// Create order
const order = await tx.order.create({
data: {
userId,
status: 'pending',
},
})

// Create order items
await tx.orderItem.createMany({
data: items.map((item) => ({
orderId: order.id,
productId: item.productId,
quantity: item.quantity,
})),
})

// Update inventory
for (const item of items) {
await tx.product.update({
where: { id: item.productId },
data: {
stock: { decrement: item.quantity },
},
})
}

return order
})
}
}

Soft Deletes

Implement soft delete pattern:

// prisma/schema.prisma
model User {
id String @id @default(cuid())
email String @unique
deletedAt DateTime?
// ...
}
// user.service.ts
@Injectable()
export class UserService {
private prisma = inject(PrismaService)

async findAll() {
return this.prisma.user.findMany({
where: { deletedAt: null },
})
}

async softDelete(id: string) {
return this.prisma.user.update({
where: { id },
data: { deletedAt: new Date() },
})
}

async restore(id: string) {
return this.prisma.user.update({
where: { id },
data: { deletedAt: null },
})
}
}

Pagination Helper

Create reusable pagination:

interface PaginationParams {
page: number
limit: number
}

interface PaginatedResult<T> {
data: T[]
meta: {
total: number
page: number
limit: number
totalPages: number
}
}

@Injectable()
export class UserService {
private prisma = inject(PrismaService)

async findAllPaginated(
params: PaginationParams,
): Promise<PaginatedResult<User>> {
const { page, limit } = params
const skip = (page - 1) * limit

const [data, total] = await Promise.all([
this.prisma.user.findMany({ skip, take: limit }),
this.prisma.user.count(),
])

return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
}
}
}