wcz-layout
Version:
183 lines (128 loc) • 5.1 kB
Markdown
name: database-schema
description: >
Design PostgreSQL tables with Drizzle ORM pgTable and derive Zod validation
schemas via createSelectSchema from drizzle-orm/zod. Covers uuid primary keys,
column types, Zod refinements (trim, min, max), and type inference. Drizzle
tables live in src/lib/db/schemas/, Zod schemas in src/schemas/. Activate
when creating or modifying database tables or validation schemas.
type: core
library: wcz-layout
library_version: "7.6.1"
sources:
- "wcz-layout:src/lib/db/schemas/"
- "wcz-layout:src/schemas/"
- "drizzle-orm:docs/zod.md"
# Database Schema Design
## Setup
Database schema files live in two directories:
```
src/lib/db/schemas/todo.ts # Drizzle pgTable definition
src/schemas/todo.ts # Zod schema derived from the table
```
## Core Patterns
### Drizzle table definition
```typescript
// src/lib/db/schemas/todo.ts
import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
export const todoTable = pgTable("todos", {
id: uuid().primaryKey(),
name: text().notNull(),
description: text(),
isCompleted: boolean().notNull().default(false),
createdBy: text().notNull(),
createdAt: timestamp().notNull().defaultNow(),
updatedAt: timestamp().notNull().defaultNow(),
});
```
All tables use `uuid().primaryKey()`. IDs are generated client-side with `uuidv7()`.
### Deriving Zod schema from Drizzle
```typescript
// src/schemas/todo.ts
import { createSelectSchema } from "drizzle-orm/zod";
import { todoTable } from "~/lib/db/schemas/todo";
export const TodoSchema = createSelectSchema(todoTable, {
name: (schema) => schema.trim().min(1),
description: (schema) => schema.trim().nullish(),
});
export type Todo = typeof TodoSchema._type;
```
`createSelectSchema` generates a Zod schema matching the table columns. The second argument lets you refine individual fields — add `.trim()`, `.min()`, `.max()`, or replace the schema entirely.
### Insert schema for mutations
```typescript
// src/schemas/todo.ts
import { createInsertSchema } from "drizzle-orm/zod";
export const TodoInsertSchema = createInsertSchema(todoTable, {
name: (schema) => schema.trim().min(1),
});
```
Use `createInsertSchema` when you need a schema for insert operations that omits fields with defaults (like `id`, `createdAt`).
### Type inference
```typescript
import type { InferSelectModel, InferInsertModel } from "drizzle-orm";
import { todoTable } from "~/lib/db/schemas/todo";
type Todo = InferSelectModel<typeof todoTable>;
type NewTodo = InferInsertModel<typeof todoTable>;
```
Prefer the Zod `_type` for runtime-validated types. Use Drizzle inference types when you need the raw DB shape without validation.
## Common Mistakes
### CRITICAL Writing Zod schemas manually instead of deriving from Drizzle
Wrong:
```typescript
import { z } from "zod";
export const TodoSchema = z.object({
id: z.string().uuid(),
name: z.string().trim().min(1),
isCompleted: z.boolean(),
});
```
Correct:
```typescript
import { createSelectSchema } from "drizzle-orm/zod";
import { todoTable } from "~/lib/db/schemas/todo";
export const TodoSchema = createSelectSchema(todoTable, {
name: (schema) => schema.trim().min(1),
});
```
Manual Zod schemas drift from the database when columns are added or renamed. `createSelectSchema` keeps them in sync automatically.
Source: maintainer interview
### HIGH Placing schema files in wrong directory
Wrong:
```
src/lib/db/schemas/todo.ts # Contains both pgTable AND Zod schema
```
Correct:
```
src/lib/db/schemas/todo.ts # Only pgTable definition
src/schemas/todo.ts # Zod schema derived from the table
```
Drizzle table definitions and Zod validation schemas live in separate directories. Mixing them in one file violates the project structure convention.
Source: consumer project structure
### HIGH Forgetting uuid primary key convention
Wrong:
```typescript
import { serial, pgTable, text } from "drizzle-orm/pg-core";
export const todoTable = pgTable("todos", {
id: serial().primaryKey(),
name: text().notNull(),
});
```
Correct:
```typescript
import { uuid, pgTable, text } from "drizzle-orm/pg-core";
export const todoTable = pgTable("todos", {
id: uuid().primaryKey(),
name: text().notNull(),
});
```
All tables use `uuid().primaryKey()`. IDs are generated client-side with `uuidv7()` for optimistic inserts through TanStack DB collections.
Source: consumer project example
### HIGH Tension: Type safety vs. rapid prototyping
Deriving Zod schemas from Drizzle tables and wiring them through forms requires more setup than quick `useState` + manual validation. Always use the enforced pattern — `createSelectSchema` + `useLayoutForm` — even for simple forms.
See also: skills/forms-validation/SKILL.md § Core Patterns
See also:
- skills/api-routes/SKILL.md — Schemas feed into validationMiddleware(Schema).
- skills/tanstack-db-collections/SKILL.md — Same Zod schema used as collection schema option.
- skills/forms-validation/SKILL.md — Zod schemas used as form validators.