I wanted the same thing again: keep unit tests fast, but enable a subset of tests to run against a real Postgres database with migrations applied automatically similar to Django's default experience. In my monorepo PoC, I wired Prisma to support that workflow.
Note: This post describes a proof of concept. The core idea works, but I'll be explicit about what's still missing for a production-grade testing setup.
Django's test experience is integrated: test runner + ORM + migrations + DB lifecycle all come together. In the TS ecosystem, Prisma is “just” the ORM layer; you still need to decide:
How to initiate the DB
When migrations run
How to reset data
How to prevent parallel tests from stepping on each other
So I built a pragmatic approach inside my monorepo PoC.
Django sets an expectation: “integration tests with a real DB should be trivial.” In Prisma + NestJS, that experience is absolutely achievable but you have to intentionally assemble the pieces.
Coming from Django, I missed the “it just works” test database story: tests can run real queries against a real DB with migrations applied automatically. In the TypeScript ecosystem, this is usually not “default behavior”, so I built a small helper for NestJS + MikroORM that gives me a Django-like workflow for integration tests while keeping unit tests fast and DB-free.
Background: what I miss from Django testing
In Django, the test runner and ORM are part of one cohesive stack:
A dedicated test database is created automatically.
Migrations (or schema setup) are handled for you.
Each test (or test class) gets isolation via transactions / database flush strategies.
The whole thing is standardized across most Django projects.
In many TS backends (NestJS + ORM + Jest/Vitest), the ecosystem is more modular. NestJS doesn’t own your ORM; the ORM doesn’t own your test runner; and the test runner doesn’t own your DB lifecycle. Result: you assemble your own conventions.
Why this isn’t “built-in” in many TS stacks (likely reasons)
A few reasons (none are “bad”, it’s mostly ecosystem shape):
Framework/ORM/test-runner separation
NestJS is framework-only; MikroORM/Prisma are external; Jest/Vitest are external. No single layer feels responsible for end-to-end test DB lifecycle.
Performance expectations differ
JS/ S culture often optimizes for fast unit tests first; integration tests exist but are intentionally explicit/opt-in.
Parallelism & isolation are harder than they look
A “shared test DB” is easy; a “shared test DB with parallel test workers, deterministic cleanup, and fast migrations” is not trivial to implement.
So instead of waiting for a universal solution, I created a small helper that matches my needs.
The PoC: DatabaseTest helper
In this PR, I added a tiny utility class that:
connects to a dedicated *_test database,
creates the DB if it doesn’t exist,
runs migrations,
and lets only certain test files opt in. 
Key idea: unit tests stay unit tests (fast, mocked). Integration tests opt into “real DB”.
The code
The helper lives at test/utils/database-test.ts and builds a MikroORM config from your app config, but with a test dbName and the migrator enabled. It ensures the database exists and migrates it.
teardown closes the connection but keeps the database content
Using it in tests
I added posts.integration.spec.ts as a minimal integration test showing how it feels to use: 
beforeAll: orm = await DatabaseTest.init()
afterAll: await DatabaseTest.close()
create an entity, persistAndFlush, clear EM, query it back, assert.
This test demonstrates real persistence + retrieval via the DB.
It also includes a second test that asserts data is still there (“keeps data between tests”). This is just here to demonstrate the default behavior but in a real world project it would be a good idea to delete the created data after each test case.
What I like about this approach (benefits)
Django-ish developer experience for integration tests: “run tests, DB exists, migrations applied”.
Opt-in integration tests: you only pay the DB cost where it matters.
No custom CLI needed: the helper is regular TypeScript code used by Jest specs.
Avoids over-mocking: you can test queries, relations, constraints, migrations… the real stuff.
Current trade-offs
This is explicitly a PoC, and it has important caveats:
No isolation by default
The DB content is kept between tests and between runs.
That’s fast, but it can create inter-test coupling unless you manage cleanup carefully.
No parallel-worker safety
If you run tests with multiple workers, they may collide on the same DB.
Migration runtime
Running migrations on test start is great, but can be slow as the migration history grows.
State drift risk
“Kept DB” can hide bugs if your schema/data evolves but the DB wasn’t reset as you expected.
What I would implement
If I were to evolve this from PoC to “real project quality”, I’d add:
Isolation strategy: truncate tables between tests/suites
Parallel test support: per-worker DB name (e.g. _test_w1, _test_w2).
Seeding/factories: a standard place for fixtures, factories, and deterministic seeds.
Closing
Django made “real DB tests” feel boring—in a good way. In NestJS + MikroORM, I had to assemble the story myself. This helper is small, explicit, and already valuable for integration tests where real queries matter.
End 2023 I introduced the first version Sports Dashboard, a minimal web app that tracks the latest results and league tables for Europe’s top football competitions. If you missed that origin story, catch up here.
That version used Angular 16 and a simple Django/REST API. It worked, but I quickly stopped enjoying working with Angular. So I decided to rebuild the project using tools I actually want to use.
2. Why rebuild?
Main reasons:
I wanted to stop using Angular.
I was learning Next.js 15 and wanted to go deeper.
I wanted to own the full stack of a GraphQL API.
I wanted to stop self-hosting and intentionally accept vendor lock-in with Vercel.
3. Wins & learnings
GraphQL turned out to be a great match for football data. One query can fetch standings, match results and team info in a single request—no overfetching.