TypeScript

Como Usar Banco de Dados com TypeScript: Prisma, TypeORM e Drizzle Comparados em Produção

18 min de leitura

Como Usar Banco de Dados com TypeScript: Prisma, TypeORM e Drizzle Comparados em Produção

Introdução ao Ecossistema de Banco de Dados em TypeScript TypeScript revolucionou a forma como desenvolvemos aplicações backend ao trazer tipagem estática para JavaScript. Quando o assunto é interação com banco de dados, três bibliotecas dominam o mercado: Prisma, TypeORM e Drizzle. Cada uma oferece uma abordagem diferente para resolver o mesmo problema: facilitar a comunicação entre sua aplicação TypeScript e o banco de dados, mantendo segurança de tipos e produtividade. O objetivo deste artigo é ajudá-lo a entender os fundamentos de cada ferramenta, suas filosofias de design e quando escolher uma em detrimento das outras. Não vamos apenas comparar números; vamos explorar como cada uma pensa diferentemente sobre o problema de persistência de dados, para que você possa tomar uma decisão informada baseada nas necessidades reais do seu projeto. Fundamentos: O que é uma ORM/Query Builder Diferença entre ORM e Query Builder Antes de mergulharmos nas três ferramentas, é crucial entender que nem todas são "ORMs" no sentido clássico. Uma

<h2>Introdução ao Ecossistema de Banco de Dados em TypeScript</h2>

<p>TypeScript revolucionou a forma como desenvolvemos aplicações backend ao trazer tipagem estática para JavaScript. Quando o assunto é interação com banco de dados, três bibliotecas dominam o mercado: Prisma, TypeORM e Drizzle. Cada uma oferece uma abordagem diferente para resolver o mesmo problema: facilitar a comunicação entre sua aplicação TypeScript e o banco de dados, mantendo segurança de tipos e produtividade.</p>

<p>O objetivo deste artigo é ajudá-lo a entender os fundamentos de cada ferramenta, suas filosofias de design e quando escolher uma em detrimento das outras. Não vamos apenas comparar números; vamos explorar como cada uma pensa diferentemente sobre o problema de persistência de dados, para que você possa tomar uma decisão informada baseada nas necessidades reais do seu projeto.</p>

<h2>Fundamentos: O que é uma ORM/Query Builder</h2>

<h3>Diferença entre ORM e Query Builder</h3>

<p>Antes de mergulharmos nas três ferramentas, é crucial entender que nem todas são &quot;ORMs&quot; no sentido clássico. Uma <strong>ORM (Object-Relational Mapping)</strong> mapeia objetos da sua linguagem para tabelas do banco de dados, abstraindo completamente o SQL. Um <strong>Query Builder</strong>, por outro lado, oferece uma interface programática para construir queries SQL sem escrever SQL bruto, mas mantendo você mais próximo do banco.</p>

<p>O Prisma ocupa uma posição interessante: é uma ORM moderna que gera código e oferece um query builder integrado. O TypeORM é uma ORM tradicional inspirada no Hibernate (Java), com decoradores. O Drizzle é um query builder leve que se recusa a ser chamado de ORM. A escolha entre eles depende de quanto você quer se afastar do SQL e quanto de overhead você está disposto a aceitar.</p>

<h3>Por que TypeScript muda o jogo</h3>

<p>TypeScript elimina a maior dor de cabeça das ORMs tradicionais: tipos perdidos. Quando você faz uma query, o resultado é tipado automaticamente. Isso significa menos erros em produção, melhor autocompletar na IDE e documentação viva do seu schema. Todas as três ferramentas aproveitam isso, mas de formas diferentes.</p>

<h2>Prisma: O Padrão Moderno</h2>

<h3>Arquitetura e Filosofia</h3>

<p>O Prisma é a ferramenta mais jovem das três e representa uma mudança de paradigma. Ele não usa decoradores nem herança de classes; em vez disso, você define seu schema em um arquivo <code>.prisma</code> declarativo e a ferramenta gera um cliente tipado. O arquivo <code>schema.prisma</code> é a fonte única da verdade para seu modelo de dados.</p>

<p>A geração de código é fundamental no design do Prisma. Quando você roda <code>prisma migrate dev</code>, o Prisma não apenas cria a migration, mas também regenera o cliente com tipos inferidos do seu schema. Isso significa que seu código TypeScript sempre está sincronizado com o banco de dados.</p>

<h3>Configuração e Exemplo Prático</h3>

<p>Vamos começar instalando as dependências:</p>

<pre><code class="language-bash">npm install @prisma/client

npm install -D prisma

npx prisma init</code></pre>

<p>Primeiro, configure sua conexão no arquivo <code>.env</code>:</p>

<pre><code class="language-env">DATABASE_URL=&quot;postgresql://user:password@localhost:5432/myapp&quot;</code></pre>

<p>Agora, defina seu schema no <code>prisma/schema.prisma</code>:</p>

<pre><code class="language-prisma">// prisma/schema.prisma

datasource db {

provider = &quot;postgresql&quot;

url = env(&quot;DATABASE_URL&quot;)

}

generator client {

provider = &quot;prisma-client-js&quot;

}

model User {

id Int @id @default(autoincrement())

email String @unique

name String?

posts Post[]

@@map(&quot;users&quot;)

}

model Post {

id Int @id @default(autoincrement())

title String

content String?

published Boolean @default(false)

author User @relation(fields: [authorId], references: [id])

authorId Int

@@map(&quot;posts&quot;)

}</code></pre>

<p>Execute as migrations:</p>

<pre><code class="language-bash">npx prisma migrate dev --name init</code></pre>

<p>Agora você pode usar o cliente gerado:</p>

<pre><code class="language-typescript">// src/index.ts

import { PrismaClient } from &#039;@prisma/client&#039;;

const prisma = new PrismaClient();

async function main() {

// Criar usuário

const user = await prisma.user.create({

data: {

email: &#039;alice@example.com&#039;,

name: &#039;Alice&#039;,

posts: {

create: [

{ title: &#039;Hello World&#039;, published: true },

{ title: &#039;Draft Post&#039;, published: false }

]

}

},

include: {

posts: true

}

});

console.log(user);

// Query com type safety

const userWithPosts = await prisma.user.findUnique({

where: { id: user.id },

include: {

posts: {

where: { published: true },

orderBy: { id: &#039;desc&#039; }

}

}

});

console.log(userWithPosts);

// Atualizar

await prisma.user.update({

where: { id: user.id },

data: { name: &#039;Alice Updated&#039; }

});

// Deletar

await prisma.post.deleteMany({

where: { authorId: user.id }

});

await prisma.user.delete({

where: { id: user.id }

});

}

main()

.catch(e =&gt; console.error(e))

.finally(() =&gt; prisma.$disconnect());</code></pre>

<h3>Vantagens do Prisma</h3>

<p>O cliente gerado oferece <strong>type safety absoluto</strong>. Se você tenta acessar um campo que não existe, o TypeScript reclama. Migrations são gerenciadas automaticamente e são seguras. O Prisma mantém histórico completo de mudanças no schema. A sintaxe é intuitiva e expressiva, com suporte robusto para relacionamentos complexos.</p>

<h3>Desvantagens</h3>

<p>O Prisma se recusa a suportar alguns padrões avançados de SQL, como queries muito complexas com múltiplas subqueries. Quando você precisa de queries raw, deve usar <code>prisma.$queryRaw</code>, que perde a segurança de tipos. A performance em operações em massa pode ser inferior a query builders puros, pois há overhead da abstração. Para projetos muito complexos com lógica SQL pesada, o Prisma pode ser limitante.</p>

<h2>TypeORM: A ORM Tradicional com TypeScript</h2>

<h3>Arquitetura e Filosofia</h3>

<p>O TypeORM segue o padrão clássico de ORM: você define entidades como classes com decoradores, e a biblioteca gerencia o mapeamento para o banco de dados. É fortemente inspirado no Hibernate (Java) e JPA, logo será familiar se você vem dessa experiência.</p>

<p>O TypeORM oferece mais controle sobre a estrutura das suas classes e permite herança, polimorfismo e padrões orientados a objetos mais complexos. Não há geração de código; você escreve tudo manualmente, o que significa menos &quot;magia&quot;, mas mais responsabilidade.</p>

<h3>Configuração e Exemplo Prático</h3>

<p>Instale as dependências:</p>

<pre><code class="language-bash">npm install typeorm reflect-metadata

npm install -D @types/node</code></pre>

<p>Configure seu banco de dados e defina as entidades:</p>

<pre><code class="language-typescript">// src/database.ts

import &#039;reflect-metadata&#039;;

import { DataSource } from &#039;typeorm&#039;;

import { User } from &#039;./entities/User&#039;;

import { Post } from &#039;./entities/Post&#039;;

export const AppDataSource = new DataSource({

type: &#039;postgres&#039;,

host: &#039;localhost&#039;,

port: 5432,

username: &#039;user&#039;,

password: &#039;password&#039;,

database: &#039;myapp&#039;,

synchronize: process.env.NODE_ENV === &#039;development&#039;,

logging: true,

entities: [User, Post],

migrations: [&#039;src/migrations/*.ts&#039;],

subscribers: [&#039;src/subscribers/*.ts&#039;]

});</code></pre>

<p>Defina as entidades:</p>

<pre><code class="language-typescript">// src/entities/User.ts

import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from &#039;typeorm&#039;;

import { Post } from &#039;./Post&#039;;

@Entity(&#039;users&#039;)

export class User {

@PrimaryGeneratedColumn()

id!: number;

@Column({ unique: true })

email!: string;

@Column({ nullable: true })

name?: string;

@OneToMany(() =&gt; Post, post =&gt; post.author, { eager: false })

posts!: Post[];

}</code></pre>

<pre><code class="language-typescript">// src/entities/Post.ts

import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from &#039;typeorm&#039;;

import { User } from &#039;./User&#039;;

@Entity(&#039;posts&#039;)

export class Post {

@PrimaryGeneratedColumn()

id!: number;

@Column()

title!: string;

@Column({ nullable: true })

content?: string;

@Column({ default: false })

published!: boolean;

@ManyToOne(() =&gt; User, user =&gt; user.posts, { onDelete: &#039;CASCADE&#039; })

@JoinColumn({ name: &#039;authorId&#039; })

author!: User;

@Column()

authorId!: number;

}</code></pre>

<p>Agora use o repositório para operações:</p>

<pre><code class="language-typescript">// src/index.ts

import &#039;reflect-metadata&#039;;

import { AppDataSource } from &#039;./database&#039;;

import { User } from &#039;./entities/User&#039;;

import { Post } from &#039;./entities/Post&#039;;

async function main() {

await AppDataSource.initialize();

const userRepository = AppDataSource.getRepository(User);

const postRepository = AppDataSource.getRepository(Post);

// Criar usuário

const user = new User();

user.email = &#039;bob@example.com&#039;;

user.name = &#039;Bob&#039;;

const savedUser = await userRepository.save(user);

// Criar posts

const post1 = new Post();

post1.title = &#039;Hello World&#039;;

post1.published = true;

post1.author = savedUser;

const post2 = new Post();

post2.title = &#039;Draft&#039;;

post2.published = false;

post2.author = savedUser;

await postRepository.save([post1, post2]);

// Query com eager loading

const userWithPosts = await userRepository.findOne({

where: { id: savedUser.id },

relations: [&#039;posts&#039;]

});

console.log(userWithPosts);

// Query builder para lógica complexa

const publishedPosts = await postRepository

.createQueryBuilder(&#039;post&#039;)

.leftJoinAndSelect(&#039;post.author&#039;, &#039;author&#039;)

.where(&#039;post.published = :published&#039;, { published: true })

.orderBy(&#039;post.id&#039;, &#039;DESC&#039;)

.getMany();

console.log(publishedPosts);

// Atualizar

await userRepository.update(savedUser.id, { name: &#039;Bob Updated&#039; });

// Deletar

await userRepository.delete(savedUser.id);

await AppDataSource.destroy();

}

main().catch(console.error);</code></pre>

<h3>Vantagens do TypeORM</h3>

<p>TypeORM oferece <strong>flexibilidade máxima</strong> em design de entidades com suporte a herança, polimorfismo e patterns OOP avançados. O Query Builder é poderoso e permite construir queries complexas mantendo type safety. Oferece subscritores e hooks de ciclo de vida. Suporta múltiplos bancos de dados diferentes (MySQL, PostgreSQL, SQLite, etc.) de forma consistente.</p>

<h3>Desvantagens</h3>

<p>Há mais boilerplate: decoradores, configuração manual, sincronização de tipos entre classe e banco é manual. O Query Builder, apesar de poderoso, é mais verbose que o Prisma. A curva de aprendizado é mais acentuada. Migrations não são tão elegantes quanto no Prisma. Performance pode sofrer em casos com muitos decoradores e reflexão.</p>

<h2>Drizzle ORM: O Query Builder Moderno</h2>

<h3>Arquitetura e Filosofia</h3>

<p>Drizzle é o mais novo e radical na sua simplicidade. Não é uma ORM; é um query builder tipado que se recusa a abstrair demais do SQL. A premissa é clara: SQL é excelente, então vamos apenas adicionar type safety e ergonomia sem esconder a lógica.</p>

<p>Drizzle gera um arquivo de tipos baseado no seu schema, semelhante ao Prisma, mas oferece mais controle. Você está sempre próximo do SQL real, o que é poderoso para queries complexas. Suporta migrations versionadas e oferece excelente performance.</p>

<h3>Configuração e Exemplo Prático</h3>

<p>Instale as dependências:</p>

<pre><code class="language-bash">npm install drizzle-orm postgres

npm install -D drizzle-kit</code></pre>

<p>Configure seu schema em <code>src/schema.ts</code>:</p>

<pre><code class="language-typescript">// src/schema.ts

import { pgTable, serial, text, boolean, integer } from &#039;drizzle-orm/pg-core&#039;;

import { relations } from &#039;drizzle-orm&#039;;

export const users = pgTable(&#039;users&#039;, {

id: serial(&#039;id&#039;).primaryKey(),

email: text(&#039;email&#039;).unique().notNull(),

name: text(&#039;name&#039;)

});

export const posts = pgTable(&#039;posts&#039;, {

id: serial(&#039;id&#039;).primaryKey(),

title: text(&#039;title&#039;).notNull(),

content: text(&#039;content&#039;),

published: boolean(&#039;published&#039;).default(false).notNull(),

authorId: integer(&#039;authorId&#039;)

.notNull()

.references(() =&gt; users.id, { onDelete: &#039;cascade&#039; })

});

export const usersRelations = relations(users, ({ many }) =&gt; ({

posts: many(posts)

}));

export const postsRelations = relations(posts, ({ one }) =&gt; ({

author: one(users, {

fields: [posts.authorId],

references: [users.id]

})

}));</code></pre>

<p>Configure o <code>drizzle.config.ts</code>:</p>

<pre><code class="language-typescript">// drizzle.config.ts

import type { Config } from &#039;drizzle-kit&#039;;

export default {

schema: &#039;./src/schema.ts&#039;,

out: &#039;./migrations&#039;,

driver: &#039;pg&#039;,

dbCredentials: {

connectionString: process.env.DATABASE_URL!

}

} satisfies Config;</code></pre>

<p>Gere as migrations:</p>

<pre><code class="language-bash">npx drizzle-kit generate:pg

npx drizzle-kit migrate</code></pre>

<p>Use o cliente:</p>

<pre><code class="language-typescript">// src/index.ts

import { drizzle } from &#039;drizzle-orm/node-postgres&#039;;

import { Pool } from &#039;pg&#039;;

import { eq, and } from &#039;drizzle-orm&#039;;

import * as schema from &#039;./schema&#039;;

const pool = new Pool({

connectionString: process.env.DATABASE_URL

});

const db = drizzle(pool, { schema });

async function main() {

// Inserir usuário

const [user] = await db

.insert(schema.users)

.values({

email: &#039;charlie@example.com&#039;,

name: &#039;Charlie&#039;

})

.returning();

console.log(&#039;User created:&#039;, user);

// Inserir posts

await db

.insert(schema.posts)

.values([

{ title: &#039;First Post&#039;, published: true, authorId: user.id },

{ title: &#039;Draft&#039;, published: false, authorId: user.id }

]);

// Select com relacionamentos

const userWithPosts = await db.query.users.findFirst({

where: eq(schema.users.id, user.id),

with: {

posts: true

}

});

console.log(&#039;User with posts:&#039;, userWithPosts);

// Query mais complexa com WHERE e ORDER BY

const publishedPosts = await db

.select()

.from(schema.posts)

.where(

and(

eq(schema.posts.published, true),

eq(schema.posts.authorId, user.id)

)

)

.orderBy(schema.posts.id);

console.log(&#039;Published posts:&#039;, publishedPosts);

// Update

await db

.update(schema.users)

.set({ name: &#039;Charlie Updated&#039; })

.where(eq(schema.users.id, user.id));

// Delete

await db

.delete(schema.users)

.where(eq(schema.users.id, user.id));

process.exit(0);

}

main().catch(console.error);</code></pre>

<h3>Vantagens do Drizzle</h3>

<p>Drizzle é <strong>extremamente leve</strong> e rápido. Não há reflexão pesada ou geração massiva de código. O schema é tipado nativamente, oferecendo type safety completo. Migrations são versionadas e versionáveis. Performance é excelente porque você está essencialmente escrevendo SQL com autocompletar. Oferece flexibilidade máxima para queries complexas sem compromissos.</p>

<h3>Desvantagens</h3>

<p>A curva de aprendizado é acentuada se você não conhece SQL bem. Relacionamentos são menos automáticos que em ORMs tradicionais; você precisa pensá-los explicitamente. Há menos abstração, então código que seria genérico no Prisma pode ser repetitivo no Drizzle. Comunidade menor significa menos recursos e exemplos.</p>

<h2>Comparação Prática e Quando Usar Cada Uma</h2>

<h3>Matriz de Decisão</h3>

<p>Para <strong>startups e MVPs</strong>: escolha <strong>Prisma</strong>. É rápido para prototipagem, migrations automáticas economizam tempo, e o cliente gerado oferece segurança com mínimo boilerplate.</p>

<p>Para <strong>aplicações empresariais complexas</strong>: escolha <strong>TypeORM</strong>. Se você precisa de padrões OOP avançados, herança de entidades, ou múltiplos bancos de dados, TypeORM oferece a flexibilidade necessária. O custo é mais boilerplate, mas a arquitetura fica mais robusta.</p>

<p>Para <strong>equipes com expertise SQL e projetos com queries pesadas</strong>: escolha <strong>Drizzle</strong>. Se seus dados são complexos e você precisa de queries otimizadas com subconsultas aninhadas, Drizzle oferece o melhor balance entre segurança de tipos e controle.</p>

<h3>Exemplo de Mesma Query em Três Ferramentas</h3>

<p>Cenário: buscar usuários com mais de 3 posts publicados e retornar usuário com posts, ordenado por data.</p>

<p><strong>Prisma:</strong></p>

<pre><code class="language-typescript">const users = await prisma.user.findMany({

where: {

posts: {

some: { published: true }

}

},

include: {

posts: {

where: { published: true },

orderBy: { createdAt: &#039;desc&#039; }

}

},

take: 10

});</code></pre>

<p><strong>TypeORM:</strong></p>

<pre><code class="language-typescript">const users = await userRepository

.createQueryBuilder(&#039;user&#039;)

.leftJoinAndSelect(&#039;user.posts&#039;, &#039;posts&#039;, &#039;posts.published = :published&#039;, { published: true })

.where((qb) =&gt; {

const subQuery = qb

.subQuery()

.select(&#039;COUNT(*)&#039;, &#039;count&#039;)

.from(Post, &#039;p&#039;)

.where(&#039;p.authorId = user.id AND p.published = :published&#039;, { published: true })

.getQuery();

return (${subQuery}) &gt; :minPosts;

}, { minPosts: 3 })

.orderBy(&#039;posts.createdAt&#039;, &#039;DESC&#039;)

.take(10)

.getMany();</code></pre>

<p><strong>Drizzle:</strong></p>

<pre><code class="language-typescript">const users = await db.query.users.findMany({

where: (users, { inArray }) =&gt; {

return inArray(

users.id,

db

.select({ userId: posts.authorId })

.from(posts)

.where(eq(posts.published, true))

.groupBy(posts.authorId)

.having(sqlcount(*) &gt; 3)

);

},

with: {

posts: {

where: eq(posts.published, true),

orderBy: posts.createdAt

}

},

limit: 10

});</code></pre>

<h2>Conclusão</h2>

<p>As três ferramentas resolvem o mesmo problema de formas distintas. <strong>Prisma</strong> é perfeito quando você quer desenvolvimento rápido com segurança de tipos automática e migrations gerenciadas. <strong>TypeORM</strong> brilha quando você precisa de arquitetura orientada a objetos sofisticada e flexibilidade em patterns de design. <strong>Drizzle</strong> é ideal quando você valoriza performance, controle fino sobre SQL e quer uma ferramenta que não se coloca no seu caminho.</p>

<p>Não existe a melhor ferramenta absoluta; existe a melhor ferramenta para seu projeto específico, sua equipe e seus requisitos. A recomendação profissional é: comece com Prisma em projetos novos, migre para TypeORM quando a complexidade exigir padrões OOP avançados, e considere Drizzle se performance em queries complexas se tornar crítica. Familiaridade com as três tornará você um desenvolvedor backend mais versátil em TypeScript.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://www.prisma.io/docs/" target="_blank" rel="noopener noreferrer">Prisma Documentation</a> - Documentação oficial do Prisma com exemplos práticos</li>

<li><a href="https://typeorm.io/" target="_blank" rel="noopener noreferrer">TypeORM Official Documentation</a> - Guia completo de TypeORM com padrões de design</li>

<li><a href="https://orm.drizzle.team/" target="_blank" rel="noopener noreferrer">Drizzle ORM Documentation</a> - Documentação e tutoriais do Drizzle</li>

<li><a href="https://www.oreilly.com/library/view/web-development-with/9781492053507/" target="_blank" rel="noopener noreferrer">Database Management in Modern Node.js Applications</a> - Capítulo sobre ORMs em Node.js</li>

<li><a href="https://www.typescriptlang.org/docs/handbook/decorators.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook: Decorators</a> - Referência oficial sobre decoradores TypeScript</li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

Boas Práticas de Escrevendo Arquivos .d.ts: Tipando Bibliotecas JavaScript Existentes para Times Ágeis
Boas Práticas de Escrevendo Arquivos .d.ts: Tipando Bibliotecas JavaScript Existentes para Times Ágeis

O que são Arquivos .d.ts e Por Que Importam Os arquivos (TypeScript Declarati...

Como Usar Keyof, Typeof e Indexed Access Types em TypeScript em Produção
Como Usar Keyof, Typeof e Indexed Access Types em TypeScript em Produção

Entendendo Keyof em TypeScript O operador é um dos recursos mais poderosos do...

O que Todo Dev Deve Saber sobre Herança e Polimorfismo em TypeScript com Classes e Interfaces
O que Todo Dev Deve Saber sobre Herança e Polimorfismo em TypeScript com Classes e Interfaces

Entendendo Herança em TypeScript A herança é um dos pilares da Programação Or...