Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Chapter 5: Database Integration
Integrating a database is a core part of any backend application. NestJS supports multiple ORMs, with TypeORM and Prisma being the most popular. This chapter covers setup, modeling, querying, transactions, migrations, relationships, and best practices for both. We’ll walk through the process step by step, so you can confidently connect your app to a database.
5.1 Choosing an ORM
Before diving into setup, understand the differences between TypeORM and Prisma to choose the right tool for your project.TypeORM vs Prisma
Choosing an ORM is one of those decisions that is hard to reverse later, so it pays to understand the trade-offs upfront. Think of it this way: TypeORM is like a Swiss Army knife — lots of tools, lots of flexibility, but you need to know which blade to pull out. Prisma is like a purpose-built power tool — fewer options, but the ones it has are exceptionally well-designed and type-safe. TypeORM:- Active Record and Data Mapper patterns — you choose the architectural style that fits your team
- Decorator-based entity definitions — feels natural if you are coming from NestJS or Angular
- Mature ecosystem with many features (subscribers, listeners, custom repositories)
- Flexible query builder for complex SQL that ORMs usually struggle with
- Works with TypeScript and JavaScript — broader adoption
- Good for complex queries, legacy databases, and teams that want fine-grained SQL control
- Type-safe database client — your IDE catches query errors before you even run the code
- Schema-first approach — a single
schema.prismafile is the source of truth for your data model - Excellent developer experience — Prisma Studio for visual data browsing, automatic migrations
- Automatic client generation — run
prisma generateand get a perfectly typed client - Great for rapid development and teams that prioritize type safety over SQL flexibility
- Strong TypeScript support — arguably the best TypeScript database experience available
- Complex queries with joins, subqueries, and raw SQL
- You prefer the decorator pattern (consistent with the rest of NestJS)
- Working with an existing database schema you cannot change
- Need database-specific features (stored procedures, materialized views)
- Starting a new project with a clean schema
- Type safety is a top priority (fewer runtime query errors)
- Rapid prototyping — schema changes and migrations are fast
- Your team is less experienced with raw SQL
Detailed Feature Comparison
| Feature | TypeORM | Prisma |
|---|---|---|
| Entity Definition | Decorators on classes | schema.prisma file |
| Type Safety | Partial (runtime errors possible) | Full (compile-time query validation) |
| Raw SQL | queryBuilder + .query() | $queryRaw (typed with Prisma.sql) |
| Migrations | Generate from entity diff, manual edit | Auto-generated from schema diff |
| Migration Rollback | Manual down() methods | prisma migrate reset (destructive) |
| Seeding | Manual scripts | prisma db seed (built-in) |
| Relations (Eager) | { eager: true } on relation | include: in query |
| Relations (Lazy) | Lazy-loaded via promises | Not supported (explicit include only) |
| Transactions | QueryRunner or DataSource.transaction | $transaction (sequential or interactive) |
| Connection Pooling | Built-in via extra config | Built-in, auto-managed |
| Multiple Databases | Multiple DataSource instances | Multiple schema.prisma files |
| MongoDB Support | Yes (experimental) | Yes (stable since Prisma 3.12) |
| Visual Database Tool | None built-in (use DBeaver, pgAdmin) | Prisma Studio (npx prisma studio) |
| Bundle Size Impact | Moderate | Large (generated client) |
| NestJS Integration | @nestjs/typeorm (official) | Manual PrismaService (simple) |
Migration Strategy Decision Framework
| Situation | Recommended Approach |
|---|---|
| Greenfield project, small team | Prisma migrate dev — fast iteration, auto-generated migrations |
| Greenfield project, large team | Either ORM + review migration SQL before applying |
| Existing database you cannot modify | TypeORM with synchronize: false — introspect and map entities to existing tables |
| Existing database, switching to Prisma | prisma db pull to introspect, then manage with Prisma going forward |
| Need custom SQL in migrations | TypeORM (migration files are plain SQL) or Prisma with prisma migrate diff + manual edits |
| Multiple environments (dev/staging/prod) | Both ORMs: apply migrations in CI pipeline, never run migrate dev in production |
5.2 Setting Up TypeORM
TypeORM is a mature ORM that works seamlessly with NestJS. Let’s set it up step by step.Installation
Basic Configuration
Using Configuration Module
Better approach using@nestjs/config:
5.3 Defining Entities with TypeORM
Entities define your database structure using decorators.Basic Entity
Entities are TypeScript classes that map to database tables. Each property decorated with@Column() becomes a column in the table. Think of an entity as a contract between your code and your database — it says “this table has these columns with these types.”
Column Options
Relationships
One-to-Many:5.4 Using Repositories with TypeORM
Repositories provide methods to interact with entities.Injecting Repository
Using Repository in Service
Advanced Queries
Query Builder:5.5 Setting Up Prisma
Prisma provides a type-safe database client with excellent developer experience.Installation
Schema Definition
Prisma Service
The PrismaService is the bridge between NestJS’s DI system and Prisma’s client. By extending PrismaClient and implementing NestJS lifecycle hooks, we get automatic connection management — the database connects when the module initializes and disconnects when the app shuts down.@Global() PrismaModule so every feature module can inject it without explicit imports. This mirrors how @nestjs/config’s ConfigModule works.
Registering Prisma Service
5.6 Using Prisma in Services
Prisma provides a type-safe, intuitive API for database operations.Basic CRUD Operations
Advanced Queries
Including Relations:5.7 Transactions
Transactions ensure multiple operations succeed or fail together.TypeORM Transactions
Using DataSource:Prisma Transactions
Sequential Operations:Transaction Edge Cases
Edge Case 1: Transactions and connection pool exhaustion Every open transaction holds a database connection. If your transaction calls an external API (payment gateway, email service) that takes 10 seconds, that connection is blocked for 10 seconds. Under load, this exhausts the connection pool and cascades into failures for all requests. Rule: keep transactions as short as possible. Do external calls before or after the transaction, not inside it.$transaction timeout
Prisma interactive transactions have a default timeout of 5 seconds. If your transaction exceeds this, it is automatically rolled back. Increase it for complex operations:
save() vs insert() in transactions
save() does a SELECT first to check if the entity exists, then INSERT or UPDATE. Inside a tight loop, this doubles your queries. Use insert() when you know the entity is new, and update() when you know it exists. save() is convenient but expensive at scale.
5.8 Migrations
Migrations keep your database schema in sync with your code.TypeORM Migrations
Generate Migration:Prisma Migrations
Create Migration:5.9 Best Practices
Following best practices ensures your database integration is robust and maintainable.Use Environment Variables
Never hard-code database credentials:Connection Pooling
Configure connection pooling for production:Use Migrations
Never usesynchronize: true in production:
Index Optimization
Add indexes for frequently queried columns:Query Optimization
TypeORM:Query Performance Decision Table
| Symptom | Likely Cause | Fix |
|---|---|---|
| Endpoint returns 50+ fields but frontend uses 5 | Over-fetching (no select) | Add select to limit returned columns |
| Loading a list of items is slow despite small result set | Missing database index | Add index on WHERE/ORDER BY columns |
| Loading an entity with relations triggers N+1 queries | Lazy loading or separate queries per relation | Use relations (TypeORM) or include (Prisma) for eager loading |
| Pagination on large tables is slow at high page numbers | OFFSET scans and discards rows | Use cursor-based pagination (WHERE id > last_seen_id) |
| Writes are slow on tables with many indexes | Each INSERT/UPDATE maintains all indexes | Review indexes, remove unused ones, consider partial indexes |
| Read-heavy endpoint causes write contention | Reads and writes compete for locks | Add a read replica, route reads to it |
| Memory usage spikes when loading large datasets | Loading all rows into memory | Stream results (createQueryBuilder().stream() in TypeORM) |
Error Handling
Database errors are cryptic by default — your users should never see “ERROR: duplicate key value violates unique constraint.” Translate database-level errors into meaningful HTTP exceptions.Testing
Use test databases for integration tests:5.10 Summary
You’ve learned how to integrate databases with NestJS: Key Concepts:- TypeORM: Decorator-based ORM with flexible queries
- Prisma: Type-safe database client with excellent DX
- Entities/Models: Define database structure
- Repositories: Abstract data access
- Relationships: One-to-one, one-to-many, many-to-many
- Transactions: Ensure data consistency
- Migrations: Version control for database schema
- Use environment variables for configuration
- Use migrations, never synchronize in production
- Optimize queries and add indexes
- Handle errors gracefully
- Use connection pooling in production
- Write integration tests
Interview Deep-Dive
Walk me through your decision process for choosing between TypeORM and Prisma for a new NestJS project.
Walk me through your decision process for choosing between TypeORM and Prisma for a new NestJS project.
Strong Answer:
- The decision starts with the team and project constraints. If the team is experienced with SQL and needs complex joins, raw queries, or stored procedures, TypeORM’s query builder gives more control. If the team prioritizes developer experience and type safety, Prisma’s generated client catches query errors at compile time.
- For greenfield projects, I lean toward Prisma. The schema-first approach means a single
schema.prismafile is the source of truth. Migrations are automatic, the generated client is fully typed, and Prisma Studio provides a visual data browser for debugging. - TypeORM’s strength is flexibility with legacy databases. If I am integrating with an existing database that has 200 tables, custom naming conventions, and stored procedures, TypeORM handles it better. Prisma’s introspection can reverse-engineer a schema, but complex views and stored procedures require workarounds.
- The setup includes: connection pooling (max connections based on available connections divided by app instances), retry logic, and a health check that pings the database.
prisma.$queryRaw. For a recursive CTE, write the raw SQL and type the result manually. This loses Prisma’s type safety for that specific query, but it is a pragmatic escape hatch. If the team needs many raw queries, consider using Prisma for simple CRUD and a lightweight query builder like Kysely for complex queries — they can share the same connection pool.Explain database transactions in NestJS. What are the gotchas with Prisma's interactive transactions?
Explain database transactions in NestJS. What are the gotchas with Prisma's interactive transactions?
Strong Answer:
- A transaction ensures multiple database operations either all succeed or all fail. In NestJS with TypeORM, use
dataSource.transaction(async (manager) => { ... }). The critical mistake is using the regular repository inside the callback — those queries run outside the transaction. - In Prisma, use
prisma.$transaction(async (tx) => { ... }). The gotcha: Prisma’s interactive transactions have a default timeout of 5 seconds. If your transaction takes longer, it is automatically rolled back. Increase with{ timeout: 30000 }. - Another gotcha: the array form
prisma.$transaction([...])runs operations in parallel without conditional logic. The callback form runs sequentially withif/elsesupport. - The production rule: keep transactions short. Never call external APIs inside a transaction — they hold database connections and locks. Fetch external data before starting the transaction, then do all writes inside it.
version column to the entity. When updating, include the version in the WHERE clause: UPDATE users SET name = 'new', version = version + 1 WHERE id = 1 AND version = 5. If another user changed the record, the WHERE matches zero rows. In Prisma, use prisma.user.update({ where: { id: 1, version: 5 }, data: { name: 'new', version: { increment: 1 } } }). Prisma throws P2025 if no rows match, which you catch and return 409 Conflict.Your NestJS API is slow and database queries are the bottleneck. Walk me through your diagnosis and optimization approach.
Your NestJS API is slow and database queries are the bottleneck. Walk me through your diagnosis and optimization approach.
Strong Answer:
- Step 1: Enable query logging. In TypeORM:
logging: true. In Prisma:prisma.$on('query', (e) => console.log(e.query, e.duration)). This shows every query and its duration. - Step 2: Look for N+1 queries — the most common ORM performance problem. Loading 100 users then loading each user’s posts in a loop is 101 queries. Fix with eager loading: TypeORM
relations: ['posts'], Prismainclude: { posts: true }. - Step 3: Check missing indexes. Run
EXPLAIN ANALYZEon slow queries. “Seq Scan” on millions of rows means you need an index. TypeORM:@Index(). Prisma:@@index([email]). - Step 4: Optimize SELECT. Only fetch needed columns. Prisma:
select: { id: true, name: true }. - Step 5: Add pagination. Never load all rows from large tables.
- Step 6: Consider caching frequent, rarely-changing queries in Redis.
dataLoader.load(userId), DataLoader collects all requested IDs and fires one WHERE author_id IN (1, 2, 3, ...) query. Register DataLoaders as request-scoped providers to avoid cache leaks between requests.Why should you never use synchronize: true in production, and what is your migration workflow?
Why should you never use synchronize: true in production, and what is your migration workflow?
Strong Answer:
synchronize: truetells TypeORM to auto-alter the schema on every startup. If you rename a column, TypeORM drops the old column (destroying data) and creates a new one. No rollback, no confirmation, no audit trail.- I witnessed a production incident where adding
nullable: trueto a column triggered anALTER TABLEon a 50-million-row table, locking it for 12 minutes and causing a complete API outage. - The correct workflow: TypeORM
migration:generatecompares entities to the database and generates SQL. You review it, commit to version control, and run withmigration:run. In Prisma:prisma migrate devcreates migration files,prisma migrate deployapplies them in production. - My CI pipeline: (1) Run migrations against a test database. (2) Run tests. (3) Build Docker image. (4) During deployment, run migrations before starting the new app version. (5) If migration fails, roll back the deployment.
pg_repack (PostgreSQL) or pt-online-schema-change (MySQL) that copy the table without locking. For PostgreSQL 11+, ALTER TABLE ADD COLUMN ... DEFAULT ... is instant (metadata-only). For indexes, use CREATE INDEX CONCURRENTLY. Plan the strategy before writing the migration file.