<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Dario Ielardi's Blog]]></title><description><![CDATA[Fullstack Web Developer & Traveler. CTO @myothis.]]></description><link>https://darioielardi.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 17 Apr 2026 09:23:10 GMT</lastBuildDate><atom:link href="https://darioielardi.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Schema-Based Multi-Tenancy with NestJS and Prisma]]></title><description><![CDATA[We recently migrated a big NestJS project to adopt Prisma as the ORM. It turned out to be a great decision, and I described our wonderful experience in a recent blog post.
The project is an API backend for a multi-tenant system, where each tenant has...]]></description><link>https://darioielardi.dev/schema-based-multi-tenancy-with-nestjs-and-prisma</link><guid isPermaLink="true">https://darioielardi.dev/schema-based-multi-tenancy-with-nestjs-and-prisma</guid><category><![CDATA[Node.js]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[multi-tenant ]]></category><dc:creator><![CDATA[Dario Ielardi]]></dc:creator><pubDate>Tue, 22 Dec 2020 09:56:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1608306587471/RWKZsNljM.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We recently migrated a big <a target="_blank" href="https://nestjs.com">NestJS</a> project to adopt <a target="_blank" href="https://prisma.io">Prisma</a> as the ORM. It turned out to be a great decision, and I described our wonderful experience in a recent <a target="_blank" href="https://darioielardi.dev/migrating-a-large-production-app-from-typeorm-to-prisma">blog post</a>.</p>
<p>The project is an API backend for a multi-tenant system, where each tenant has its own schema in our Postgres database.</p>
<h2 id="heading-the-problem">The problem</h2>
<p>In an ideal scenario, we would just need to specify the database schema for every performed query. Unfortunately, Prisma does not support that at the moment. However, it allows passing the database connection string when the <code>PrismaClient</code> is instantiated, skipping the one declared in the <code>.env</code> file. Thanks to that and the power of NestJS we came up with a quite solid solution.</p>
<h2 id="heading-the-solution">The solution</h2>
<p>We decided to dedicate a separate instance of <code>PrismaClient</code> to each tenant schema. Now, obviously, we cannot create an instance for every request, we better find a way to cache instances and reuse them when possible.</p>
<p>It's important to note that in our case we need to handle a relatively small amount of tenants, and we are also working with a database that can handle a large number of connections. This solution would not have been a good fit otherwise, since every instance of <code>PrismaClient</code> requires its own connection to the database.</p>
<p>The idea is to have a <em>manager</em> class that will hold all created instances, provide them, and will eventually create new ones when needed. It would also have to dispose every created instance on server shutdown to free up connections.</p>
<p>Let's now take a look at how we implemented this solution.</p>
<h2 id="heading-the-implementation">The implementation</h2>
<p>The <em>manager</em> class is pretty straightforward. It is going to be a simple NestJS provider, holding a "cache" object for instantiated clients as a class property.</p>
<blockquote>
<p>In our case tenant IDs are used as database schema names, but you might also provide your own mapping logic.</p>
</blockquote>
<p>We need three methods here: </p>
<ol>
<li><code>getTenantId</code>: this will extract the tenant id from the request object. In our case, it will be extracted from the JWT in the <code>Authorization</code> header, but it may also be deduced from the hostname.</li>
<li><code>getClient</code>: this will be called for every request to retrieve a tenant-dedicated <code>PrismaClient</code> instance, and it does most of the work.</li>
<li><code>onModuleDestroy</code>: this will be called <a target="_blank" href="https://docs.nestjs.com/fundamentals/lifecycle-events">by the framework</a> on application shutdown, and it will be in charge of clients disposal.</li>
</ol>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { Injectable, OnModuleDestroy } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { PrismaClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@prisma/client'</span>;
<span class="hljs-keyword">import</span> { Request } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;

<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> PrismaClientManager <span class="hljs-keyword">implements</span> OnModuleDestroy {
  <span class="hljs-comment">// the client instances cache object</span>
  <span class="hljs-keyword">private</span> clients: { [key: <span class="hljs-built_in">string</span>]: PrismaClient } = {};

  getTenantId(request: Request): <span class="hljs-built_in">string</span> {
    <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> retrieve and return the tenant ID from the request object,</span>
    <span class="hljs-comment">// eventually throw an exception if the ID is not valid</span>
  }

  getClient(request: Request): PrismaClient {
    <span class="hljs-keyword">const</span> tenantId = <span class="hljs-built_in">this</span>.getTenantId(request);
    <span class="hljs-keyword">let</span> client = <span class="hljs-built_in">this</span>.clients[tenantId];

    <span class="hljs-comment">// create and cache a new client when needed</span>
    <span class="hljs-keyword">if</span> (!client) {
      <span class="hljs-keyword">const</span> databaseUrl = process.env.DATABASE_URL!.replace(<span class="hljs-string">'public'</span>, tenantId);

      client = <span class="hljs-keyword">new</span> PrismaClient({
        datasources: {
          db: {
            url: databaseUrl,
          },
        },
      });

      <span class="hljs-comment">// setup prisma middlewares if any</span>

      <span class="hljs-built_in">this</span>.clients[tenantId] = client;
    }

    <span class="hljs-keyword">return</span> client;
  }

  <span class="hljs-keyword">async</span> onModuleDestroy() {
    <span class="hljs-comment">// wait for every cached instance to be disposed</span>
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(
      <span class="hljs-built_in">Object</span>.values(<span class="hljs-built_in">this</span>.clients).map(<span class="hljs-function">(<span class="hljs-params">client</span>) =&gt;</span> client.$disconnect()),
    );
  }
}
</code></pre>
<p>You may also want to implement type safety and validation for tenant IDs.</p>
<p>Note that the code above assumes the existence of a <code>DATABASE_URL</code> environment variable holding a database connection string with the <code>schema</code> query param set to <code>public</code>, like the one in the <code>.env</code> file generated by Prisma:</p>
<pre><code class="lang-bash">DATABASE_URL=<span class="hljs-string">"postgresql://user:password@localhost:5432/mydb?schema=public"</span>
</code></pre>
<p>Also, make sure to add this provider to a global module, like a <code>DatabaseModule</code> or the <code>AppModule</code>.</p>
<p>Now, the last thing is to find a way to call <code>getClient</code> for every request and pass around the instance based on where we need it.</p>
<h3 id="heading-first-approach">First approach</h3>
<p>The first, really verbose approach is to access the request object from every handler method in every controller and pass it around to every service method we need to call to handle the request.</p>
<pre><code class="lang-ts"><span class="hljs-comment">// controller example method</span>
  <span class="hljs-meta">@Get</span>()
  findAll(<span class="hljs-meta">@Req</span>() request: Request) {
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.service.findAll(request);
  }
</code></pre>
<pre><code class="lang-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> MyService {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> prismaClientManager: PrismaClientManager</span>) {}

  findAll(request: Request) {
    <span class="hljs-keyword">const</span> prisma = <span class="hljs-built_in">this</span>.prismaClientManager.getClient(request);
    <span class="hljs-comment">// method logic ...</span>
  }
}
</code></pre>
<p>This is extremely verbose, we can definitely do better.</p>
<h3 id="heading-second-approach">Second approach</h3>
<p>Thanks to the NestJS dependency injection system, every registered provider is instantiated the first time it needs to be injected, and then the instance is reused any subsequent time. However, we can take advantage of a built-in <a target="_blank" href="https://docs.nestjs.com/fundamentals/injection-scopes">mechanism</a> to make a service request-scoped, asking the framework to create a new instance for each incoming request.</p>
<p>To do that we need to add a <code>scope</code> parameter to the <code>Injectable</code> decorator. This way we will also have access to the incoming request object.</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { Injectable, Scope, Inject } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { REQUEST } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/core'</span>;
<span class="hljs-keyword">import</span> { Request } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> { PrismaClientManager } <span class="hljs-keyword">from</span> <span class="hljs-string">'../database/prisma-client-manager'</span>;

<span class="hljs-meta">@Injectable</span>({ scope: Scope.REQUEST })
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> MyService {
  <span class="hljs-keyword">private</span> prisma: PrismaClient;

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">
    <span class="hljs-meta">@Inject</span>(REQUEST) request: Request,
    prismaClientManager: PrismaClientManager,
  </span>) {
    <span class="hljs-built_in">this</span>.prisma = prismaClientManager.getClient(request);
  }

  findAll() {
    <span class="hljs-comment">// just use this.prisma to access the database</span>
  }
}
</code></pre>
<p>This is less verbose and easier to manage than the first approach, but we still need to add the code above to all of the services from which we need to access the Prisma client. </p>
<p>Let's try to take advantage of dependency injection again to avoid that.</p>
<h3 id="heading-third-approach-and-final-solution">Third approach and final solution</h3>
<p>Turns out all we need is a <a target="_blank" href="https://docs.nestjs.com/fundamentals/custom-providers#factory-providers-usefactory">custom factory provider</a>, which will be the only explicitly request-scoped component and will be called by the framework to provide us a <code>PrismaClient</code> whenever we need it to be injected as a service dependency.</p>
<pre><code class="lang-ts"><span class="hljs-comment">// src/database/database.module.ts</span>

<span class="hljs-keyword">import</span> { FactoryProvider, Global, Module, Scope } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { REQUEST } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/core'</span>;
<span class="hljs-keyword">import</span> { PrismaClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@prisma/client'</span>;
<span class="hljs-keyword">import</span> { Request } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> { PrismaClientManager } <span class="hljs-keyword">from</span> <span class="hljs-string">'./prisma-client-manager'</span>;

<span class="hljs-keyword">const</span> prismaClientProvider: FactoryProvider&lt;PrismaClient&gt; = {
  provide: PrismaClient,
  scope: Scope.REQUEST,
  inject: [REQUEST, PrismaClientManager],
  useFactory: <span class="hljs-function">(<span class="hljs-params">request: Request, manager: PrismaClientManager</span>) =&gt;</span> manager.getClient(request),
};

<span class="hljs-meta">@Global</span>()
<span class="hljs-meta">@Module</span>({
  providers: [PrismaClientManager, prismaClientProvider],
  <span class="hljs-built_in">exports</span>: [PrismaClient],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> DatabaseModule {}
</code></pre>
<p>This way we can totally forget about the request-scoped logic and simply use the <code>PrismaClient</code> in our services as if it was a regular provider.</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { PrismaClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@prisma/client'</span>;

<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> MyService {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> prisma: PrismaClient</span>) {}

  findAll() {
    <span class="hljs-comment">// just use this.prisma to access the database</span>
  }
}
</code></pre>
<p>The providers with a dependency on the <code>PrismaClient</code> will still be request-scoped, but we don't need to make it explicit anymore, we can just use them as regular providers.</p>
<p>A complete example of the final implementation can be found in <a target="_blank" href="https://github.com/darioielardi/nestjs-prisma-multitenant">this repo</a>.</p>
<p>This is how we achieved multi-tenancy with Prisma in a NestJS project.
Please don't hesitate to ask any questions or leave a comment.</p>
<blockquote>
<p>Note: the strategy described above is the same adopted by <a target="_blank" href="https://github.com/Errorname/prisma-multi-tenant">this great library</a>. If you don't want to write your own implementation or you'd like to take advantage of other features such as a multi-tenant-aware migrations CLI, I highly suggest taking a look at it.</p>
</blockquote>
<p>Cheers!</p>
]]></content:encoded></item><item><title><![CDATA[Migrating a Large Production App From TypeORM To Prisma]]></title><description><![CDATA[One of the last projects we worked on at Myothis is a property management system for a big client.
It's a pretty large codebase, and we built the backend with the NestJS framework, which is database agnostic but provides strong integration with TypeO...]]></description><link>https://darioielardi.dev/migrating-a-large-production-app-from-typeorm-to-prisma</link><guid isPermaLink="true">https://darioielardi.dev/migrating-a-large-production-app-from-typeorm-to-prisma</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[Databases]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[orm]]></category><dc:creator><![CDATA[Dario Ielardi]]></dc:creator><pubDate>Tue, 15 Dec 2020 10:24:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1607789267847/U7kmtSyI4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One of the last projects we worked on at <a target="_blank" href="https://myothis.com">Myothis</a> is a property management system for a big client.</p>
<p>It's a pretty large codebase, and we built the backend with the <a target="_blank" href="https://nestjs.com">NestJS</a> framework, which is database agnostic but provides strong integration with TypeORM, which is why I've always decided to use it when I'm working with NestJS.</p>
<p>Since the <a target="_blank" href="https://prisma.io">Prisma</a> team announced V2 I've kept an eye open on their Twitter profile, and when they announced the beta version of Prisma Client I've started to play around and got pretty excited about it. I almost immediately felt like I was looking at a truly next-gen approach to data access in NodeJS, which I always felt could be more than improved.</p>
<p>Prisma became generally available as stable in June 2020, and since there they constantly released new versions with bug fixes and lots of improvements and new features. After only six months it felt utopian to use it in production, especially for our big project, as I was pretty sure there were a lot of edge cases it couldn't be ready to handle. However I was too curious to give it a serious try, and one day, as an experiment, I decided to migrate a single part of the backend and see where it would take me.</p>
<p>The results impressed me so much, the next day we started to migrate the whole project.</p>
<h3 id="heading-first-steps">First steps</h3>
<blockquote>
<p>Please note: the migration process is explained in detail <a target="_blank" href="https://www.prisma.io/docs/guides/migrate-to-prisma/migrate-from-typeorm">here</a> in the Prisma docs. For this reason, I'll focus on my experience rather than the exact steps and the code.</p>
</blockquote>
<p>The very first step to introduce Prisma to an existing codebase is <em>schema introspection</em>. It takes a single command for Prisma to analyze your database schema and fill out the <code>schema.prisma</code> file with model definitions, each one corresponding to a table in your schema. There were a couple of things to fix, mostly related to naming and relations handling, but since we're talking about a thirty-two tables schema with every possible form of relation involved, it did a pretty good job.</p>
<p>Now, I decided to make a couple of updates to our database schema to better fit Prisma conventions, to take the most advantage of code generation and avoid current limitations, in particular:</p>
<ul>
<li><p>Join tables need to follow some <a target="_blank" href="https://www.prisma.io/docs/concepts/components/prisma-schema/relations#many-to-many-relations">conventions</a> to represent <em>implicit relations</em>, which means that Prisma will handle the relation <strong>completely</strong> behind the scenes, reducing complexity a lot. At the same time we can also have <em>explicit</em> many-to-many relations in case we need to add extra data to the join table.</p>
</li>
<li><p>JSON columns are supported, but query capabilities are pretty limited at the time of writing ( <a target="_blank" href="https://www.notion.so/Prisma-Roadmap-50766227b779464ab98899accb98295f?p=5acca22f9a474ab4a8f67e19d412cc25">this might change soon</a> ). That's why I decided to get rid of every JSON column, simply changing them to scalar columns or adding new tables. This might be a problem for some cases, but fortunately not for ours.</p>
</li>
</ul>
<p>I also want to spend some words on the editing experience for the Prisma schema. Initially, I didn't like the idea of a separate schema with a new syntax, but I must say the <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=Prisma.prisma">VSCode extension</a> does an impressive job at making things fast and easy to edit, other than giving you hints and warnings about any issue and even fixing those for you.</p>
<h3 id="heading-nestjs-integration">NestJS integration</h3>
<blockquote>
<p>If you are not interested specifically in NestJS you can safely skip this paragraph. NestJS basic knowledge is required to understand what I'm about to describe.</p>
</blockquote>
<p>Thanks to the NestJS dependency injection system Prisma integration is smooth, and it turns out to be less verbose than TypeORM.</p>
<p>With TypeORM we need to:</p>
<ol>
<li>Register <code>TypeOrmModule</code> in a global Nest module, with the configuration object.</li>
<li>Every time a new entity is introduced we need to register it in the <code>entities</code> array ( this may be skipped with <a target="_blank" href="https://docs.nestjs.com/techniques/database#auto-load-entities">entities auto-loading</a> ).</li>
<li>Every time we need to use TypeORM repositories in a Nest module we need to add <code>TypeOrmModule.forFeature([EntityClass])</code> to the <code>imports</code> array for that module.</li>
<li>For every TypeORM repository we need to use in a provider class we need to add this to the class constructor:<pre><code class="lang-ts"><span class="hljs-keyword">constructor</span>(<span class="hljs-params">
 <span class="hljs-meta">@InjectRepository</span>(User)
 <span class="hljs-keyword">private</span> usersRepository: Repository&lt;User&gt;,
</span>) {}
</code></pre>
Prisma integration, on the other hand, is pretty straightforward. We just need an injectable <code>PrismaService</code> extending the generated <code>PrismaClient</code>, add it to a global module, and it can now be used anywhere we need.</li>
</ol>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { Injectable, OnModuleInit, OnModuleDestroy } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { PrismaClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@prisma/client'</span>;

<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> PrismaService <span class="hljs-keyword">extends</span> PrismaClient <span class="hljs-keyword">implements</span> OnModuleInit, OnModuleDestroy {

  <span class="hljs-keyword">async</span> onModuleInit() {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.$connect();
  }

  <span class="hljs-keyword">async</span> onModuleDestroy() {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.$disconnect();
  }
}
</code></pre>
<pre><code class="lang-ts"><span class="hljs-comment">// any provider we need to access prisma from </span>
<span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> prisma: PrismaService</span>) {}
</code></pre>
<p>More on the NestJS - Prisma combo can be found on the Prisma <a target="_blank" href="https://www.prisma.io/nestjs">website</a>.</p>
<h3 id="heading-migrations">Migrations</h3>
<p>Prisma also provides <a target="_blank" href="https://www.prisma.io/docs/concepts/components/prisma-migrate">Prisma Migrate</a>, which is a very promising tool to handle migrations, but it is not considered production-ready at the time of writing. That's why we decided to rely on an external tool for migrations, which is out of the scope of this article.</p>
<p>At the time of writing, they've just announced a <a target="_blank" href="https://www.prisma.io/blog/prisma-migrate-preview-b5eno5g08d0b">release preview</a>, and I can't wait to try it out and write down my first impressions.</p>
<p>I've also had some not so nice experiences with TypeORM migrations generation, which is why I've always ended up using the CLI to generate empty files and then filling them up myself with raw SQL.
Because of that, giving it up wasn't difficult at all.</p>
<h2 id="heading-developer-experience">Developer Experience</h2>
<p>The developer experience working with Prisma was definitely above my team's expectations.</p>
<h3 id="heading-code-readability">Code readability</h3>
<p>With TypeORM, most of the queries end up being split into multiple parts, with a lot of the query builder methods scattered all over the function's body. This really hurts code readability, and it takes some time for anyone to fully understand what the query is doing. 
With Prisma, even the most complex query fits in an extremely well-structured object parameter, making it possible even for developers not really experienced with SQL to easily understand the purpose of the query.</p>
<h3 id="heading-consistency-and-predictability">Consistency and predictability</h3>
<p>It is pretty common with TypeORM to start with a particular approach on a single query and then switch to another one because of some limitation or unpredictable behavior. There are lots of ways to do the same thing, and it may be confusing for newcomers to figure out which one is the best. With Prisma, there is a single, predictable, and intuitive way to perform any possible operation.</p>
<h3 id="heading-type-safety">Type safety</h3>
<p>Although TypeORM is written in TypeScript, type safety is not really guaranteed. The type of a record in a query result set will always be the defined model class, regardless of any column selection or joined relation. Moreover, the query builder requires passing tables and column names as strings, which to me feels like the complete opposite of type safety.</p>
<p>Prisma generates advanced types based on the <code>schema.prisma</code> model, and thanks to that it manages to provide type safety at its best.</p>
<p>For a query like this:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.db.user.findMany({
  select: {
    email: <span class="hljs-literal">true</span>,
    active: <span class="hljs-literal">true</span>,
    zones: {
      select: {
        name: <span class="hljs-literal">true</span>,
        properties: {
          select: {
            name: <span class="hljs-literal">true</span>,
            address: <span class="hljs-literal">true</span>,
          },
        },
      },
    },
  },
});
</code></pre>
<p>The result type would be:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> users: {
    email: <span class="hljs-built_in">string</span>;
    active: <span class="hljs-built_in">boolean</span>;
    zones: {
      name: <span class="hljs-built_in">string</span>;
      properties: {
        name: <span class="hljs-built_in">string</span>;
        address: <span class="hljs-built_in">string</span>;
      }[];
    }[];
}[]
</code></pre>
<p>As you can see, the result type is extremely precise, and it perfectly mirrors the query selection. I highly recommend taking a look at the generated types to better understand how the powerful TypeScript types system has been exploited.</p>
<p>I can confidently say this is the first time I feel like I'm working with a truly type-safe ORM.</p>
<p>More on this and the following paragraphs can be found <a target="_blank" href="https://www.prisma.io/docs/concepts/more/comparisons/prisma-and-typeorm">here</a> in the Prisma docs comparison.</p>
<h3 id="heading-relations-handling">Relations handling</h3>
<p>Managing relations has always been painful even with ORMs, and TypeORM makes no exception. For deeply nested and complex queries we end up writing a lot of <code>.leftJoinAndSelect()</code> methods because of the limitations of the ORM capabilities.</p>
<p>This is what a select query with multiple JOINs looks like with Prisma:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> prisma.user.findMany({
  include: {
    posts: {
      include: {
        categories: {
          include: {
            posts: <span class="hljs-literal">true</span>,
          },
        },
      },
    },
  },
});
</code></pre>
<p>Of course, we can add filters and custom selection on every level of the query.</p>
<p>Another example from the Prisma docs: this is a query to retrieve all users records where all posts are published and at least one related post mentions "Prisma":</p>
<pre><code class="lang-ts"><span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> prisma.user.findMany({
  where: {
    post: {
      every: {
        published: <span class="hljs-literal">true</span>
      },
      some: {
        content: {
          contains: <span class="hljs-string">"Prisma"</span>
        },
      },
    },
  },
});
</code></pre>
<p>Basically, developers don't even need to know how a JOIN works to handle complex queries with relations involved.</p>
<h3 id="heading-raw-queries">Raw queries</h3>
<p>Honestly, I expected I'd need to fall back to raw SQL queries all the time. It turned out I was very wrong. While with TypeORM I always needed to give up on the ORM and leverage the query builder, with half of the clauses as non-typed raw strings, with Prisma I only needed to write raw queries a couple of times for complex aggregation queries. I'm talking about really complex aggregations, since simpler ones are <a target="_blank" href="https://www.prisma.io/docs/concepts/components/prisma-client/aggregations">nicely supported</a>.</p>
<p>Even for those cases where raw SQL is required, Prisma <a target="_blank" href="https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access">provides</a> some nice helpers and hints to help us write raw queries, such as tagged templates helpers and utility functions and constants.</p>
<p>One thing that I miss from TypeORM is the possibility to mix type-safe query functions and raw SQL clauses, but I understand this is something the team is trying to avoid because of the consequences it might have on the developer experience and the wrong behavior it might lead to.</p>
<h3 id="heading-transactions">Transactions</h3>
<p>Prisma lacks a way to handle long-running transactions the way we are used to writing them, such as with multiple operations encapsulated within a callback or batched within a single promise.</p>
<p>This looked like a severe limitation, but we realized any time we need a transaction is because of a sequence of write operations performed atomically, which is something Prisma has always supported with a query like this:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">await</span> prisma.post.create({
  data: {
    title: <span class="hljs-string">'Post Title'</span>,
    tags: {
      create: [
        { title: <span class="hljs-string">'Tag #1'</span> },
        { title: <span class="hljs-string">'Tag #2'</span> },
        { title: <span class="hljs-string">'Tag #3'</span> },
      ],
    },
  },
});
</code></pre>
<p>At the time of writing, they are also working on a new <a target="_blank" href="https://www.prisma.io/docs/concepts/components/prisma-client/transactions#bulk-operations-preview">transaction api</a> which might cover every other use case, available right now as a preview feature.</p>
<p>I highly recommend reading <a target="_blank" href="https://www.prisma.io/blog/how-prisma-supports-transactions-x45s1d5l0ww1">this article</a> by the Prisma team to go deeper into this topic.</p>
<h3 id="heading-documentation">Documentation</h3>
<p>One of the pain points of TypeORM is documentation. I understand that is probably because of the time this library has been available and the number of features. Nonetheless, it lacks lots of things and it is very confusing. Every time you need to look for something you always end up giving up on the docs and search somewhere else.</p>
<p>Prisma is much newer, but the documentation has also been very well curated. Thanks to the intuitive structure and the powerful search tool, is always easy to find what you need.</p>
<h2 id="heading-current-limitations">Current limitations</h2>
<p>There are also some things we had to workaround. Please note that most of these issues are being worked on at the time of writing, and they might be solved when you read this.</p>
<ul>
<li>Advanced query capabilities for JSON columns, as already mentioned, are not yet supported.</li>
<li>Cascade deletes present some problems. Even when the foreign key has been declared with an <code>ON DELETE CASCADE</code> clause, Prisma prevents you from deleting the record. This can be solved with a simple raw query: <pre><code class="lang-ts">prisma.$executeRaw<span class="hljs-string">`DELETE FROM "Post" WHERE "id" = <span class="hljs-subst">${id}</span>`</span>.
</code></pre>
</li>
<li>We needed to implement soft delete for some of our entities, and although they provide an <a target="_blank" href="https://www.prisma.io/docs/concepts/components/prisma-client/middleware/soft-delete-middleware">example middleware</a> it has some flaws we needed to deal with. I mentioned that on Twitter and the team reached to me to ask for feedback and help me tackle down the issues, which is something I really appreciate and value.</li>
</ul>
<h2 id="heading-conclusions">Conclusions</h2>
<p>We are really happy with the decision we made. We reduced our codebase by thousands of lines of code, and those of us who struggled with data access are now confidently working even on complex queries.</p>
<p>Even if Prisma is relatively new, it already provides almost all the features we need, and development is constantly active! They solve lots of issues for every release, some of the issues we had at the beginning of the migration were solved when we finished.</p>
<p>With the recent 2.13 release, we can also import generated types in the browser, which paves the way for exciting solutions to some issues we have with our current monorepo setup, but this is for another blog post...</p>
<p>TypeORM was a great companion in our journey and I really appreciate the work of the community on such a big and ambitious piece of software, but for us the time came to replace it with a next-gen solution.</p>
<p>I'm excited about the future of Prisma and thankful for what the team is doing for us, and I also can't wait to know what you think about all the things you just read.</p>
<p>Cheers!</p>
]]></content:encoded></item></channel></rss>