Skip to main content

Server Actions with Prisma ORM

What is Prisma ORM?

Prisma is a modern ORM (Object-Relational Mapping) tool designed to make database operations safer and more efficient. When used with Next.js Server Actions, it allows you to perform database operations directly on the server side.

Advantages of Prisma:

  • Type Safety: Provides full integration with TypeScript
  • Auto-completion: Offers suggestions based on your database schema in your IDE
  • Schema-Driven Development: Define your database schema in a single file
  • Migrations: Safely manage database schema changes
  • Database Agnostic: Support for PostgreSQL, MySQL, SQLite, SQL Server, and MongoDB

Installation and Configuration

1. Add Prisma to Your Project

npm install prisma @prisma/client
npx prisma init

2. Configure Database Connection

Define your database connection URL in the .env file:

DATABASE_URL="postgresql://username:password@localhost:5432/mydb"

3. Define Prisma Schema

Define your database models in the prisma/schema.prisma file:

generator client {
provider = "prisma-client-js"
}

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

model Entity {
id String @id @default(uuid())
name String
description String?
created_at DateTime @default(now()) @map("created_at")
updated_at DateTime @updatedAt @map("updated_at")
}

4. Generate Prisma Client

npx prisma generate

5. Create and Apply Database Migration

npx prisma migrate dev --name init

Using Prisma with Server Actions

Prisma Client Singleton

First, create a helper file to instantiate Prisma Client as a singleton:

// lib/prisma.ts
import { PrismaClient } from '@prisma/client';

const globalForPrisma = global as unknown as { prisma: PrismaClient };

export const prisma = globalForPrisma.prisma || new PrismaClient();

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

Server Action Examples

Below are example server actions created using Prisma ORM:

'use server';

import { prisma } from '@/lib/prisma';
import { revalidatePath } from 'next/cache';
import { ServerActionResponse } from '@/types/types';
import { Entity } from '@/types/types';

// Get all entities
export async function getEntities(): Promise<ServerActionResponse<Entity[]>> {
try {
const entities = await prisma.entity.findMany();

// Transform Prisma model to our application type
const transformedEntities: Entity[] = entities.map((entity) => ({
id: entity.id,
name: entity.name,
description: entity.description || undefined,
created_at: entity.created_at.toISOString(),
updated_at: entity.updated_at.toISOString(),
}));

return {
data: transformedEntities,
status: 200,
message: 'Entities retrieved successfully',
};
} catch (error) {
console.error('Error getting entities => ', error);

return {
error: 'Unexpected error getting entities',
status: 500,
message: 'Unexpected error',
};
}
}

// Get entity by ID
export async function getEntityById(
entityId: string
): Promise<ServerActionResponse<Entity>> {
try {
const entity = await prisma.entity.findUnique({
where: { id: entityId },
});

if (!entity) {
return {
status: 404,
message: 'Entity not found',
};
}

// Transform Prisma model to our application type
const transformedEntity: Entity = {
id: entity.id,
name: entity.name,
description: entity.description || undefined,
created_at: entity.created_at.toISOString(),
updated_at: entity.updated_at.toISOString(),
};

return {
data: transformedEntity,
status: 200,
message: 'Entity retrieved successfully',
};
} catch (error) {
console.error('Error getting entity => ', error);

return {
error: 'Unexpected error getting entity',
status: 500,
message: 'Unexpected error',
};
}
}

// Create new entity
export async function createEntity(data: {
name: string;
description?: string;
}): Promise<ServerActionResponse<Entity>> {
try {
const entity = await prisma.entity.create({
data: {
name: data.name,
description: data.description,
},
});

// Transform Prisma model to our application type
const transformedEntity: Entity = {
id: entity.id,
name: entity.name,
description: entity.description || undefined,
created_at: entity.created_at.toISOString(),
updated_at: entity.updated_at.toISOString(),
};

// Clear cache
revalidatePath('/entities');

return {
data: transformedEntity,
status: 201,
message: 'Entity created successfully',
};
} catch (error) {
console.error('Error creating entity => ', error);

return {
error: 'Unexpected error creating entity',
status: 500,
message: 'Unexpected error',
};
}
}

// Update entity
export async function updateEntity({
entityId,
data,
}: {
entityId: string;
data: {
name?: string;
description?: string | null;
};
}): Promise<ServerActionResponse<Entity>> {
try {
// Check if entity exists
const existingEntity = await prisma.entity.findUnique({
where: { id: entityId },
});

if (!existingEntity) {
return {
status: 404,
message: 'Entity to update not found',
};
}

const entity = await prisma.entity.update({
where: { id: entityId },
data,
});

// Transform Prisma model to our application type
const transformedEntity: Entity = {
id: entity.id,
name: entity.name,
description: entity.description || undefined,
created_at: entity.created_at.toISOString(),
updated_at: entity.updated_at.toISOString(),
};

// Clear cache
revalidatePath('/entities');
revalidatePath(`/entities/${entityId}`);

return {
data: transformedEntity,
status: 200,
message: 'Entity updated successfully',
};
} catch (error) {
console.error('Error updating entity => ', error);

return {
error: 'Unexpected error updating entity',
status: 500,
message: 'Unexpected error',
};
}
}

// Delete entity
export async function deleteEntity(
entityId: string
): Promise<ServerActionResponse<Entity>> {
try {
// Check if entity exists
const existingEntity = await prisma.entity.findUnique({
where: { id: entityId },
});

if (!existingEntity) {
return {
status: 404,
message: 'Entity to delete not found',
};
}

const entity = await prisma.entity.delete({
where: { id: entityId },
});

// Transform Prisma model to our application type
const transformedEntity: Entity = {
id: entity.id,
name: entity.name,
description: entity.description || undefined,
created_at: entity.created_at.toISOString(),
updated_at: entity.updated_at.toISOString(),
};

// Clear cache
revalidatePath('/entities');

return {
data: transformedEntity,
status: 200,
message: 'Entity deleted successfully',
};
} catch (error) {
console.error('Error deleting entity => ', error);

return {
error: 'Unexpected error deleting entity',
status: 500,
message: 'Unexpected error',
};
}
}

Advanced Queries

Prisma provides powerful query capabilities. Here are some examples:

Filtering

// Find entities with a specific name
const entities = await prisma.entity.findMany({
where: {
name: {
contains: 'search term',
mode: 'insensitive', // Case-insensitive search
},
},
});

// Find entities created after a specific date
const recentEntities = await prisma.entity.findMany({
where: {
created_at: {
gte: new Date('2023-01-01'), // Greater than or equal to
},
},
});

Sorting

// Sort entities by name in ascending order
const sortedEntities = await prisma.entity.findMany({
orderBy: {
name: 'asc',
},
});

// Sort entities by creation date in descending order (newest first)
const newestEntities = await prisma.entity.findMany({
orderBy: {
created_at: 'desc',
},
});

Pagination

// Get the first 10 entities
const firstPage = await prisma.entity.findMany({
take: 10,
});

// Get the next 10 entities (skip the first 10)
const secondPage = await prisma.entity.findMany({
skip: 10,
take: 10,
});

// Cursor-based pagination (more efficient for large datasets)
const afterCursor = await prisma.entity.findMany({
take: 10,
cursor: {
id: 'last-entity-id-from-previous-page',
},
});

Transactions

Prisma allows you to perform multiple database operations atomically:

const [entityA, entityB] = await prisma.$transaction([
prisma.entity.create({
data: {
name: 'Entity A',
description: 'First entity in transaction',
},
}),
prisma.entity.create({
data: {
name: 'Entity B',
description: 'Second entity in transaction',
},
}),
]);

For more complex operations:

const result = await prisma.$transaction(async (tx) => {
// tx is an instance of the prisma client used within the transaction
const entityA = await tx.entity.create({
data: {
name: 'Entity A',
description: 'First entity in transaction',
},
});

const entityB = await tx.entity.create({
data: {
name: 'Entity B',
description: `Related to ${entityA.name}`,
},
});

// Both operations succeed or fail together
return { entityA, entityB };
});

Best Practices

  1. Type Safety: Ensure type safety by using the types generated by Prisma and mapping them to your application types.

  2. Error Handling: Perform all database operations within try-catch blocks.

  3. Optimize Database Connections: Use Prisma Client as a singleton.

  4. Selective Queries: Select only the fields you need, especially when working with large datasets.

  5. Caching: Properly clear cache using revalidatePath or revalidateTag.

  6. Migrations: Manage schema changes with Prisma Migrate.

  7. Security: Validate user inputs using schema validation libraries like Zod.

  8. Type Transformation: Always transform Prisma model types to your application types before returning data.

Useful Resources