Database schema design is the foundation of any data-driven application. In this lesson, we'll explore relational database concepts, understand ORMs and their benefits, dive deep into migrations, and build a complete schema for our habit tracking API using Drizzle ORM.
Relational databases organize data into tables with rows and columns, connected through relationships:
<aside> 🔗
Core Relational Concepts:
Object-Relational Mapping (ORM) translates between database tables and JavaScript objects:
// Raw SQL queries
const result = await client.query(
'SELECT u.*, h.* FROM users u LEFT JOIN habits h ON [u.id](<http://u.id>) = h.user_id WHERE [u.id](<http://u.id>) = $1',
[userId]
)
// Manual result parsing
const user = {
id: result.rows[0].id,
email: result.rows[0].email,
habits: [result.rows.map](<http://result.rows.map>)(row => ({
id: row.habit_id,
name: row.habit_name
}))
}
Problems:
// Type-safe, intuitive queries
const user = await db.query.users.findFirst({
where: eq([users.id](<http://users.id>), userId),
with: {
habits: true
}
})
// Perfect TypeScript types
[user.email](<http://user.email>) // ✅ string
user.habits[0].id // ✅ string
user.nonExistent // ❌ TypeScript error
Benefits:
Benefit | Description | Impact |
---|---|---|
Type Safety | Compile-time error checking | Fewer runtime bugs, better DX |
Query Building | Programmatic query construction | Dynamic filtering, pagination |
Relationship Loading | Automatic joins and includes | Less code, optimized queries |
Schema Management | Version-controlled database changes | Reliable deployments |
Migration System | Automated schema evolution | Safe production updates |
Migrations are versioned scripts that modify your database schema over time: