Drizzle ORM: Not Null Constraint Violation— Fixed [2026]
Also covers: .notNull() · .default() · .defaultNow() · $defaultFn · nullable columns · drizzle-kit push
⚡ The Error
PostgresError: null value in column "created_at" of relation "posts"
violates not-null constraint
// OR:
PostgresError: null value in column "user_id" violates not-null constraint✅ Fix — add .notNull().default() or provide value in insert
// Add .defaultNow() for timestamp columns
createdAt: timestamp("created_at").notNull().defaultNow(),
// OR provide the value explicitly in every insert
await db.insert(posts).values({ title, userId, createdAt: new Date() })What Causes Not Null Constraint Violations in Drizzle
When you define a column with .notNull() in Drizzle, the database enforces that every inserted row must have a non-null value for that column. The error fires when an INSERT or UPDATE omits the column and there is no database-level DEFAULT to fall back to.
Add .notNull() and .default() to Schema Columns
Core schema pattern for required fields with defaultsimport { pgTable, text, timestamp, uuid, boolean, integer } from "drizzle-orm/pg-core"
import { createId } from "@paralleldrive/cuid2"
export const posts = pgTable("posts", {
// Primary key — auto-generated
id: uuid("id").primaryKey().defaultRandom(),
// Required fields — no default — must provide in every insert
title: text("title").notNull(),
userId: uuid("user_id").notNull().references(() => users.id),
// Optional field — nullable (no .notNull())
excerpt: text("excerpt"),
// Boolean with default
published: boolean("published").notNull().default(false),
// Integer with default
viewCount: integer("view_count").notNull().default(0),
// Timestamps — auto-set by database
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
})Provide All Required Fields in Insert Payload
Not null column with no default — must supply value// ❌ Missing required userId — violates not null
await db.insert(posts).values({ title: "Hello World" })
// ✅ Provide all required fields
await db.insert(posts).values({
title: "Hello World",
userId: user.id, // required — no default
published: false, // optional — has default(false), but explicit is fine
// createdAt omitted — database fills defaultNow()
})
// ✅ Returning — get the inserted row back
const [newPost] = await db
.insert(posts)
.values({ title: "Hello", userId: user.id })
.returning()
console.log(newPost.id, newPost.createdAt) // auto-filled by DBUse .defaultNow() for Timestamp Columns
createdAt / updatedAt always null on insertimport { timestamp } from "drizzle-orm/pg-core"
export const posts = pgTable("posts", {
// createdAt — set once at insert time
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
// updatedAt — update on every change using Drizzle's hook
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
// $onUpdate fires on every UPDATE query automatically
})
// After running drizzle-kit push, both columns are set automatically:
await db.insert(posts).values({ title: "Hello", userId: user.id })
// createdAt and updatedAt are set by DB + Drizzle — no need to pass themUse JS-Level Defaults with $defaultFn
Generated IDs, slugs, random valuesimport { pgTable, text, varchar } from "drizzle-orm/pg-core"
import { createId } from "@paralleldrive/cuid2"
import slugify from "slugify"
export const posts = pgTable("posts", {
// CUID2 id — generated in JS, not SQL
id: varchar("id", { length: 128 })
.primaryKey()
.$defaultFn(() => createId()),
// Slug generated from title at insert time
slug: text("slug")
.notNull()
.$defaultFn(() => slugify(Math.random().toString())),
// Better: compute slug from title before inserting
})
// Usage — id and slug are computed by Drizzle before INSERT
const [post] = await db.insert(posts).values({ title: "Hello", userId: user.id }).returning()
console.log(post.id) // "clxxxxxxxxxxxxxx" — cuid2
console.log(post.slug) // generated slugUse $defaultFn for IDs and computed values that need JavaScript logic. Use .default() for simple static defaults (false, 0, 'draft'). Use .defaultNow() specifically for timestamp columns.
Sync Schema Changes with drizzle-kit push
Schema updated but database still has old column definitionAfter changing your schema file, you must push the changes to the database. Without this, the database still enforces the old column constraints.
# Push schema directly to DB (development — no migration files)
npx drizzle-kit push
# OR generate migration files (production-safe)
npx drizzle-kit generate
npx drizzle-kit migrate
# Check what drizzle-kit will change before applying
npx drizzle-kit push --dry-run
# Verify current schema vs DB
npx drizzle-kit checkAlways run drizzle-kit push (or generate + migrate) after changing your schema. Drizzle's TypeScript schema and the actual database schema can drift — this is the #1 cause of unexpected constraint errors in development.
Prevention
- Mark every required column with .notNull() in your schema — Drizzle infers TypeScript types from this
- Add .default() or .defaultNow() to columns that should have automatic values
- Run drizzle-kit push after every schema change during development
- Use .returning() on inserts to confirm what was actually saved
- Keep schema column modifiers in this order: type → .notNull() → .default() → .references()
- Use $defaultFn for JavaScript-computed defaults like CUID2, nanoid, or slug generation
Frequently Asked Questions
What does 'not null constraint violation' mean in Drizzle ORM?+−
How do I make a column required in Drizzle ORM?+−
What is the difference between .default() and .$defaultFn() in Drizzle?+−
How do I auto-set createdAt in Drizzle ORM?+−
Why does my Drizzle schema look correct but I still get not null errors?+−
Can I have a column that is not null but has no default?+−
Need Expert Help?
We Build Production Apps with Drizzle ORM
Softplix engineers design Drizzle schemas, write migrations, and build type-safe database layers for Next.js and Node.js. Let us help.
Talk to an Engineer