Django-style test DBs in NestJS: my MikroORM integration test helper (PoC)
2025-09-11
Re‑building Sports Dashboard: From Angular + REST to Next.js + GraphQL
2025-05-07
2025-09-11
2025-05-07
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.
In Django, the test runner and ORM are part of one cohesive stack:
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.
A few reasons (none are “bad”, it’s mostly ecosystem shape):
So instead of waiting for a universal solution, I created a small helper that matches my needs.
In this PR, I added a tiny utility class that:
*_test database,Key idea: unit tests stay unit tests (fast, mocked). Integration tests opt into “real DB”.
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.
Important implementation details from the PoC:
dbName becomes ${commonMikroOrmConfig.dbName}_testextensions: [Migrator] enables migrationsensureDatabase() creates the DB if missinggetMigrator().up() applies pending migrationsteardown closes the connection but keeps the database contentI 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()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.
This is explicitly a PoC, and it has important caveats:
If I were to evolve this from PoC to “real project quality”, I’d add:
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.
PoC source: PR #1 in abel-castro/blog-nest .
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.
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.
Here the new URL: http://sports-dashboard.abelcastro.dev. The code is also publicly available here.
Over time, I’ve shared a few posts about how my blog evolved into the Next.js project it is today. In this post, I want to dive into a recent architectural improvement and explain more about how I seamlessly switched my blog’s data source from a REST API to a GraphQL API by modifying just a handful of files.
This shift was possible thanks to the use of data providers in my project. By consistently interacting with an abstraction layer (activeDataProvider), I was able to decouple my data-fetching logic from the actual source of the data.
The beauty of this design lies in its simplicity. To change the data provider, all I had to do was:
IDataProvider interface.activeDataProvider.That’s it! No need to rewrite logic across multiple components or refactor complex parts of the application.
Here’s what the current iteration of the IDataProvider interface looks like:
export interface IDataProvider {
getAll(options: PostSearchOptions): Promise<PaginatedPosts>;
getOneBySlug(slug: string): Promise<Post | null>;
getPostMetadata(slug: string): Promise<Post | null>;
create?(data: Partial<Post>): Promise<Post>;
update?(slug: string, data: Partial<Post>): Promise<Post | null>;
delete?(slug: string): Promise<boolean>;
}
This interface enforces the core methods required for interacting with my blog’s data—fetching posts, retrieving metadata, and optionally creating, updating, or deleting posts.
BaseDataProviderTo ensure consistency across different data providers, I implemented a BaseDataProvider class:
This class handles all the standard methods from the interface and introduces an extra layer of abstraction by defining abstract methods that subclasses must implement:
export abstract class BaseDataProvider implements IDataProvider {
create?(data: Partial<Post>): Promise<Post> {
throw new Error('Method not implemented.');
}
update?(slug: string, data: Partial<Post>): Promise<Post | null> {
throw new Error('Method not implemented.');
}
delete?(slug: string): Promise<boolean> {
throw new Error('Method not implemented.');
}
abstract getAllFromStorage(
options: PostSearchOptions,
): Promise<PaginatedPosts>;
abstract getOneBySlugFromStorage(slug: string): Promise<Post | null>;
abstract getPostMetadataFromStorage(slug: string): Promise<Post | null>;
async getAll(: ): <> {
( (resolve, reject) => {
paginatedPosts = .(options);
(paginatedPosts);
});
}
(: ): < | > {
( (resolve, reject) => {
matchingPost = .(slug);
(matchingPost) {
(matchingPost);
} {
();
}
});
}
(: ): < | > {
( (resolve, reject) => {
matchingPost = .(slug);
(matchingPost) {
(matchingPost);
} {
();
}
});
}
}
These <Something>FromStorage methods are the real magic—they encapsulate the logic for interacting with the actual data source, whether it’s a REST API, a GraphQL endpoint, or even static files.
On the other hand, the BaseDataProvider provides the generic methods getAll, getOneBySlug and getPostMetadata. These are the methods that our components interact with directly. Internally, they call the appropriate getAllFromStorage, getOneBySlugFromStorage and getPostMetadataFromStorage methods.
This separation ensures that the specific details of the persistence layer are abstracted away from the components, keeping the architecture clean and decoupled.
In my opinion, decoupling components from external dependencies is a powerful asset that allows us to create more resilient and testable code. While this approach introduces some overhead and requires a shift in mindset, it proves invaluable in fast-paced environments where technologies evolve and change rapidly.
By creating abstraction layers, we can make our code more adaptable, enabling smoother transitions to new tools or data sources without major rewrites. This flexibility ultimately helps future-proof the project and maintain long-term efficiency.
Of course, one could argue that maintaining this level of abstraction is easy in a small project like my blog. But my perspective is this: if I can apply such high coding standards to a personal project that generates no profit and where no one will complain if it breaks tomorrow, why shouldn’t I hold myself to the same (or even higher) standard in my professional work?
In a business environment, where the software directly contributes to revenue and people rely on the services I build, maintaining clean, adaptable, and well-structured code is even more critical.
This is an example of a component tightly coupled to the implementation of the data layer, in this case using Apollo GraphQL:
const PostList: FC = () => {
const { data } = useQuery<PostsData, PostsVars>(GET_POSTS, {
variables: { limit: 10, offset: 0 },
});
return (
<ul>
{data?.posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.text}</p>
</li>
))}
</ul>
);
};
An one that uses the approach that I propose:
const PostList: FC = () => {
const { data } = await activeDataProvider.getAll(options);
return (
<ul>
{data?.posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.text}</p>
</li>
))}
</ul>
);
};
This example does not include any imports related to GraphQL or other external dependencies. It relies solely on the activeDataProvider interface, ensuring the component remains decoupled from the underlying data-fetching implementation.
Another benefit of this approach is how easily a data provider can be replaced by writing unit tests.
Since my app relies on activeDataProvider, I can easily swap the real data provider with an in-memory mock during unit tests.
In my vitest.setup.ts file, I added a mock that replaces activeDataProvider with a lightweight, in-memory provider:
// Replace active dataProvider with MemoryDataProvider
vi.mock('./data-providers/active', async () => {
const jsonData = JSON.parse(
readFileSync('./tests/test-data.json', 'utf-8'),
);
return {
default: new MemoryDataProvider(jsonData),
};
});
This mock loads data from a static JSON file during tests, ensuring predictable results without external dependencies.
Testing a component that fetches posts is as simple as calling:
await activeDataProvider.getAll(options);
Since the provider is mocked, the tests run fast, and I can easily simulate various data states (empty results, errors, or populated lists).
During this latest iteration, I transitioned the blog to pull data from a GraphQL API by implementing a new provider class that extends BaseDataProvider. By implementing the abstract methods (getAllFromStorage, etc.) with GraphQL queries, the switch was complete.
Now, for example, whenever a component fetches posts, it calls:
await activeDataProvider.getAll(options);
The underlying provider handles the communication with the persistance layer, ensuring the component is agnostic to whether the data came from GraphQL, REST, or elsewhere.
Checkout the complete source code here.