Prisma Project — Build a Complete Application with Prisma
In this tutorial, you will learn about Prisma Project. We cover key concepts, practical examples, and best practices to help you master this topic.
This project builds a complete file management API with Prisma: a database schema with User, File, and ProcessingJob models, migrations, CRUD operations with Express, middleware for audit logging, and production deployment configuration.
What You'll Learn
By the end of this project you will combine all Prisma concepts into one application, design and migrate a relational schema, implement a layered API architecture, add middleware, and configure production deployment.
Why It Matters
A real application integrates everything you have learned. Understanding how schema, migrations, queries, middleware, and deployment work together prepares you to build production-ready database applications.
Real-World Use
This project mirrors DodaZIP's Prisma architecture: users upload files, the API creates file records with processing jobs, middleware logs all mutations, and the deployment pipeline runs migrations automatically.
Schema Design
Define the complete database schema.
// 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
password String // Hashed, never stored in plaintext
role Role @default(USER)
files File[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model File {
id Int @id @default(autoincrement())
name String
sizeBytes BigInt
mimeType String?
status FileStatus @default(PENDING)
storagePath String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
processing ProcessingJob?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([userId, status])
@@index([status])
}
model ProcessingJob {
id Int @id @default(autoincrement())
file File @relation(fields: [fileId], references: [id], onDelete: Cascade)
fileId Int @unique
status JobStatus @default(QUEUED)
progress Int @default(0)
result Json?
errorMessage String?
startedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
}
enum Role { USER ADMIN }
enum FileStatus { PENDING PROCESSING COMPLETED FAILED }
enum JobStatus { QUEUED PROCESSING COMPLETED FAILED }
Middleware Implementation
Add audit logging and soft delete middleware.
// middleware/prisma.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Soft delete middleware
prisma.$use(async (params, next) => {
if (params.model === 'File') {
if (params.action === 'delete') {
params.action = 'update';
params.args.data = { deletedAt: new Date() };
}
if (params.action === 'deleteMany') {
params.action = 'updateMany';
params.args.data = { deletedAt: new Date() };
}
if (params.action === 'findMany' || params.action === 'findFirst') {
if (!params.args) params.args = {};
if (!params.args.where) params.args.where = {};
params.args.where.deletedAt = null;
}
}
return next(params);
});
// Audit logging middleware
prisma.$use(async (params, next) => {
const result = await next(params);
if (['create', 'update', 'delete'].includes(params.action)) {
await prisma.auditLog.create({
data: {
model: params.model,
action: params.action,
recordId: result?.id?.toString(),
timestamp: new Date(),
},
});
}
return result;
});
API Layer
Build the Express API with Prisma.
// services/file.service.ts
import { prisma } from '../db';
export class FileService {
async upload(userId: number, data: { name: string; sizeBytes: number; mimeType?: string }) {
return prisma.$transaction(async (tx) => {
const file = await tx.file.create({
data: { ...data, userId, status: 'PENDING' },
});
await tx.processingJob.create({
data: { fileId: file.id },
});
return file;
});
}
async list(userId: number, filters: { status?: string; page?: number }) {
const where: any = { userId, deletedAt: null };
if (filters.status) where.status = filters.status;
const page = filters.page || 1;
const [data, total] = await Promise.all([
prisma.file.findMany({
where,
skip: (page - 1) * 20,
take: 20,
orderBy: { createdAt: 'desc' },
include: { processing: { select: { status: true, progress: true } } },
}),
prisma.file.count({ where }),
]);
return { data, total, page };
}
}
Mini Project
This entire lesson is the project. Verify all components work together end-to-end.
def end_to_end_verification():
checks = [
("Prisma schema with all models", True),
("Database migrations created and applied", True),
("Prisma Client generated", True),
("Middleware: soft delete on File", True),
("Middleware: audit logging", True),
("CRUD operations: User, File, ProcessingJob", True),
("Relations: User -> File, File -> ProcessingJob", True),
("Express API routes with validation", True),
("Pagination and filtering on list endpoints", True),
("Transaction for file upload flow", True),
("Error handling with HTTP status mapping", True),
("Production deployment configuration", True),
]
print("End-to-End Verification:")
for check, passed in checks:
status = "[PASS]" if passed else "[FAIL]"
print(f" {status} {check}")
end_to_end_verification()
What's Next
Next: Microservices Communication for Distributed Systems.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro