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 ORM (Object-Relational Mapping) mapeia objetos da sua linguagem para tabelas do banco de dados, abstraindo completamente o SQL. Um Query Builder, por outro lado, oferece uma interface programática para construir queries SQL sem escrever SQL bruto, mas mantendo você mais próximo do banco.
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.
Por que TypeScript muda o jogo
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.
Prisma: O Padrão Moderno
Arquitetura e Filosofia
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 .prisma declarativo e a ferramenta gera um cliente tipado. O arquivo schema.prisma é a fonte única da verdade para seu modelo de dados.
A geração de código é fundamental no design do Prisma. Quando você roda prisma migrate dev, 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.
Configuração e Exemplo Prático
Vamos começar instalando as dependências:
npm install @prisma/client
npm install -D prisma
npx prisma init
Primeiro, configure sua conexão no arquivo .env:
DATABASE_URL="postgresql://user:password@localhost:5432/myapp"
Agora, defina seu schema no prisma/schema.prisma:
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
@@map("users")
}
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("posts")
}
Execute as migrations:
npx prisma migrate dev --name init
Agora você pode usar o cliente gerado:
// src/index.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// Criar usuário
const user = await prisma.user.create({
data: {
email: 'alice@example.com',
name: 'Alice',
posts: {
create: [
{ title: 'Hello World', published: true },
{ title: 'Draft Post', 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: 'desc' }
}
}
});
console.log(userWithPosts);
// Atualizar
await prisma.user.update({
where: { id: user.id },
data: { name: 'Alice Updated' }
});
// Deletar
await prisma.post.deleteMany({
where: { authorId: user.id }
});
await prisma.user.delete({
where: { id: user.id }
});
}
main()
.catch(e => console.error(e))
.finally(() => prisma.$disconnect());
Vantagens do Prisma
O cliente gerado oferece type safety absoluto. 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.
Desvantagens
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 prisma.$queryRaw, 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.
TypeORM: A ORM Tradicional com TypeScript
Arquitetura e Filosofia
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.
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 "magia", mas mais responsabilidade.
Configuração e Exemplo Prático
Instale as dependências:
npm install typeorm reflect-metadata
npm install -D @types/node
Configure seu banco de dados e defina as entidades:
// src/database.ts
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { User } from './entities/User';
import { Post } from './entities/Post';
export const AppDataSource = new DataSource({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'user',
password: 'password',
database: 'myapp',
synchronize: process.env.NODE_ENV === 'development',
logging: true,
entities: [User, Post],
migrations: ['src/migrations/*.ts'],
subscribers: ['src/subscribers/*.ts']
});
Defina as entidades:
// src/entities/User.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { Post } from './Post';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id!: number;
@Column({ unique: true })
email!: string;
@Column({ nullable: true })
name?: string;
@OneToMany(() => Post, post => post.author, { eager: false })
posts!: Post[];
}
// src/entities/Post.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './User';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn()
id!: number;
@Column()
title!: string;
@Column({ nullable: true })
content?: string;
@Column({ default: false })
published!: boolean;
@ManyToOne(() => User, user => user.posts, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'authorId' })
author!: User;
@Column()
authorId!: number;
}
Agora use o repositório para operações:
// src/index.ts
import 'reflect-metadata';
import { AppDataSource } from './database';
import { User } from './entities/User';
import { Post } from './entities/Post';
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 = 'bob@example.com';
user.name = 'Bob';
const savedUser = await userRepository.save(user);
// Criar posts
const post1 = new Post();
post1.title = 'Hello World';
post1.published = true;
post1.author = savedUser;
const post2 = new Post();
post2.title = 'Draft';
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: ['posts']
});
console.log(userWithPosts);
// Query builder para lógica complexa
const publishedPosts = await postRepository
.createQueryBuilder('post')
.leftJoinAndSelect('post.author', 'author')
.where('post.published = :published', { published: true })
.orderBy('post.id', 'DESC')
.getMany();
console.log(publishedPosts);
// Atualizar
await userRepository.update(savedUser.id, { name: 'Bob Updated' });
// Deletar
await userRepository.delete(savedUser.id);
await AppDataSource.destroy();
}
main().catch(console.error);
Vantagens do TypeORM
TypeORM oferece flexibilidade máxima 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.
Desvantagens
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.
Drizzle ORM: O Query Builder Moderno
Arquitetura e Filosofia
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.
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.
Configuração e Exemplo Prático
Instale as dependências:
npm install drizzle-orm postgres
npm install -D drizzle-kit
Configure seu schema em src/schema.ts:
// src/schema.ts
import { pgTable, serial, text, boolean, integer } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').unique().notNull(),
name: text('name')
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
published: boolean('published').default(false).notNull(),
authorId: integer('authorId')
.notNull()
.references(() => users.id, { onDelete: 'cascade' })
});
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts)
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id]
})
}));
Configure o drizzle.config.ts:
// drizzle.config.ts
import type { Config } from 'drizzle-kit';
export default {
schema: './src/schema.ts',
out: './migrations',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL!
}
} satisfies Config;
Gere as migrations:
npx drizzle-kit generate:pg
npx drizzle-kit migrate
Use o cliente:
// src/index.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import { eq, and } from 'drizzle-orm';
import * as schema from './schema';
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: 'charlie@example.com',
name: 'Charlie'
})
.returning();
console.log('User created:', user);
// Inserir posts
await db
.insert(schema.posts)
.values([
{ title: 'First Post', published: true, authorId: user.id },
{ title: 'Draft', 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('User with posts:', 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('Published posts:', publishedPosts);
// Update
await db
.update(schema.users)
.set({ name: 'Charlie Updated' })
.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);
Vantagens do Drizzle
Drizzle é extremamente leve 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.
Desvantagens
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.
Comparação Prática e Quando Usar Cada Uma
Matriz de Decisão
Para startups e MVPs: escolha Prisma. É rápido para prototipagem, migrations automáticas economizam tempo, e o cliente gerado oferece segurança com mínimo boilerplate.
Para aplicações empresariais complexas: escolha TypeORM. 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.
Para equipes com expertise SQL e projetos com queries pesadas: escolha Drizzle. 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.
Exemplo de Mesma Query em Três Ferramentas
Cenário: buscar usuários com mais de 3 posts publicados e retornar usuário com posts, ordenado por data.
Prisma:
const users = await prisma.user.findMany({
where: {
posts: {
some: { published: true }
}
},
include: {
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' }
}
},
take: 10
});
TypeORM:
const users = await userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'posts', 'posts.published = :published', { published: true })
.where((qb) => {
const subQuery = qb
.subQuery()
.select('COUNT(*)', 'count')
.from(Post, 'p')
.where('p.authorId = user.id AND p.published = :published', { published: true })
.getQuery();
return `(${subQuery}) > :minPosts`;
}, { minPosts: 3 })
.orderBy('posts.createdAt', 'DESC')
.take(10)
.getMany();
Drizzle:
const users = await db.query.users.findMany({
where: (users, { inArray }) => {
return inArray(
users.id,
db
.select({ userId: posts.authorId })
.from(posts)
.where(eq(posts.published, true))
.groupBy(posts.authorId)
.having(sql`count(*) > 3`)
);
},
with: {
posts: {
where: eq(posts.published, true),
orderBy: posts.createdAt
}
},
limit: 10
});
Conclusão
As três ferramentas resolvem o mesmo problema de formas distintas. Prisma é perfeito quando você quer desenvolvimento rápido com segurança de tipos automática e migrations gerenciadas. TypeORM brilha quando você precisa de arquitetura orientada a objetos sofisticada e flexibilidade em patterns de design. Drizzle é ideal quando você valoriza performance, controle fino sobre SQL e quer uma ferramenta que não se coloca no seu caminho.
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.
Referências
- Prisma Documentation - Documentação oficial do Prisma com exemplos práticos
- TypeORM Official Documentation - Guia completo de TypeORM com padrões de design
- Drizzle ORM Documentation - Documentação e tutoriais do Drizzle
- Database Management in Modern Node.js Applications - Capítulo sobre ORMs em Node.js
- TypeScript Handbook: Decorators - Referência oficial sobre decoradores TypeScript