← Back to blog

Engineering blog

Why I Built a Full HR System to Learn Drizzle ORM

Side projects work better when they solve real problems. Here's why building PeoplePulse's leave management system was the only way I actually understood Drizzle.

8 min read

Why I Built a Full HR System to Learn Drizzle ORM

Every tutorial I tried for Drizzle ORM showed me the same users table. Select, insert, update. OK great, I understand a select. What I didn't understand was how to model a real business domain — leave requests, approval hierarchies, balance deductions, manager overrides — with an ORM that has no built-in relation resolver like Prisma.

So I stopped doing tutorials and started building PeoplePulse instead.

The problem with toy projects for learning ORMs

With a toy project, you never hit the edges. You never need a cascaded soft delete across four tables. You never need a transaction that deducts leave balance, creates an approval record, and fires a notification event — all or nothing. You never need to explain to TypeScript why db.select().from(leaveRequests).leftJoin(users, eq(leaveRequests.employeeId, users.id)) returns a union type that's painful to work with.

With a real HR system, you hit every one of those in the first week.

What building the real system taught me

Transactions are where Drizzle actually shines. The db.transaction(async (tx) => {...}) API is clean. Cleaner than anything I'd used in Prisma for multi-step writes. Once I understood that the tx object carries the same interface as db, I stopped trying to fight it and just composed operations naturally.

The generated types are both the best and worst thing. Every query in Drizzle infers its return type from the schema. This is genuinely great until you do a join — then you get a flat object with colliding column names. I ended up writing a few small utility types to reshape the query output into something the frontend could consume. Not elegant, but real.

Schema-first thinking changed how I approach the database. In Prisma, you describe models. In Drizzle, you describe tables. That difference in framing made me think more carefully about column types, nullability, and indexing upfront rather than treating the DB as just a place to store JSON.

What PeoplePulse ended up being

By the time I finished, PeoplePulse had:

  • Full leave lifecycle: request → manager approval → balance deduction → employee notification
  • Two-role access model (employee / manager) with JWT-based auth
  • An admin override mode for HR directors
  • A full test suite with Vitest + MSW mocking the DB responses
  • A Fastify API that I'm genuinely proud of

None of that was the goal when I started. The goal was to understand Drizzle. But this is how I learn things — I need the pressure of a real use case to understand why a tool makes the choices it does.

Should you do this?

Honestly, yes. Pick a domain you know a bit about — project management, inventory, HR, whatever — and build a real backend for it using the tech you want to learn. Tutorials give you syntax. Real problems give you intuition. You need both, but you can't skip the second one.