Skip to Content
Realtime KanbanDatabase Setup with Prisma and Zod Generator

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.

prisma/schema.prisma
// 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:

tsconfig.json
{ "compilerOptions": { "paths": { "@schemas/*": ["./prisma/generated/schemas/*"], }, }, }
Last updated on