Designing the Database with Prisma and Zod Generator
Application state normalization extracts entities from server responses by their entityType and id properties. So each table in the database has an entityType column with a fixed value: the entity type.
The database has two tables: User and Task. The EntityType enum sets the entityType column to user or task: the entity name in lowercase, singular. In each table this column has a default value. Ideally, it should be read-only.
prisma-zod-generator generates Zod schemas from the Prisma models, so you don’t write them by hand and the server code stays short.
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
output = "./generated/client"
}
generator zod {
provider = "prisma-zod-generator"
config = "./zod-generator.config.json"
}
datasource db {
provider = "postgresql"
}
model User {
/// @zod.brand<'user'>()
id String @id @default(uuid())
/// @zod.custom.use(z.literal('user'))
entityType EntityType @default(user)
/// @zod.meta({ description: "Timestamp when the user was created", examples: ["2023-01-01T00:00:00.000Z"] })
createdAt DateTime @default(now())
/// @zod.meta({ description: "Timestamp when the user was last updated", examples: ["2023-01-01T00:00:00.000Z"] })
updatedAt DateTime @default(now()) @updatedAt
/// @zod.meta({ examples: ["John Doe"], description: "Full name of the user" })
fullName String
/// @zod.meta({ examples: ["john.doe@example.com"], description: "Email address of the user" })
email String @unique
/// @zod.meta({ examples: ["https://example.com/image.jpg"], description: "Profile image URL of the user" })
imageUrl String?
tasks Task[]
embedding Unsupported("vector(1536)")?
}
model Task {
/// @zod.brand<'task'>()
id String @id @default(uuid())
/// @zod.custom.use(z.literal('task'))
entityType EntityType @default(task)
/// @zod.meta({ description: "Timestamp when the task was created", examples: ["2023-01-01T00:00:00.000Z"] })
createdAt DateTime @default(now())
/// @zod.meta({ description: "Timestamp when the task was last updated", examples: ["2023-01-01T00:00:00.000Z"] })
updatedAt DateTime @default(now()) @updatedAt
/// @zod.meta({ examples: ["Implement authentication"], description: "Title of the task" })
title String
/// @zod.meta({ examples: ["Implement user authentication using JWT"], description: "Description of the task" })
description String
/// @zod.meta({ examples: ["TODO"], description: "Status of the task" })
status TaskStatus @default(TODO)
/// @zod.brand<'user'>().meta({ examples: ["a3bb189e-8bf9-3888-9912-ace4e6543002"], description: "ID of the user who owns the task" })
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
embedding Unsupported("vector(1536)")?
}
enum EntityType {
user
task
}
enum TaskStatus {
TODO
IN_PROGRESS
IN_REVIEW
DONE
}The code above is fetched from GitHub repository.
Note the triple-slash comments. React components and other app code work with entity IDs, so IDs of different entity types must be told apart for type safety. Branded types do this.
The entityType columns use literal types, and the fields have examples and a description for the generated OpenAPI documentation and for AI tools.
Each time you run npx prisma generate, the Zod schemas, with their types, are written to the prisma/generated/schemas folder.
A path mapping in tsconfig.json makes the generated schemas easier to import:
{
"compilerOptions": {
"paths": {
"@schemas/*": ["./prisma/generated/schemas/*"],
},
},
}