O Sequelize v7 é o ORM mais antigo do ecossistema Node.js — battle-tested desde 2011 e com suporte TypeScript melhorado na versão 7 via decorators embutidos em @sequelize/core/decorators-legacy e tipos nativos.
O modelo de definição usa classes que estendem Model (API nativa) ou decorators embutidos via @sequelize/core/decorators-legacy; associações são declaradas com HasMany, BelongsTo, HasOne e BelongsToMany.
Eager loading com include é a solução para evitar N+1 queries — passar required: false controla se o join é LEFT ou INNER, e aninhar include em mais de 3 níveis é sinal de problema de modelagem.
Em 2026, o Sequelize ainda é relevante para projetos legacy e equipes que já dominam sua API, mas Prisma e Drizzle são preferidos para projetos novos pela DX superior e melhor type safety.
O que é
O Sequelize é o ORM mais antigo do ecossistema Node.js, lançado em 2011 quando callbacks eram o padrão absoluto. Ao longo de mais de uma década evoluiu por Promises, async/await e, na versão 7 (2025), chegou a um suporte TypeScript muito mais robusto — com tipos embutidos sem necessidade de @types/sequelize e remoção de métodos deprecated que acumulavam desde a era legada.
Suporta os principais bancos de dados relacionais:
PostgreSQL (dialeto mais completo, incluindo JSONB, arrays, full-text search)
MySQL e MariaDB
SQLite (ótimo para testes e desenvolvimento local)
Microsoft SQL Server (diferencial em relação a Drizzle e Prisma, que têm suporte mais limitado)
O núcleo do Sequelize é baseado em modelos: classes TypeScript que mapeiam tabelas. A partir desses modelos, o ORM gera SQL para todas as operações CRUD, gerencia pool de conexões e suporta transações.
Como funciona
Definição de models
Há dois estilos de definição de modelos no ecossistema Sequelize em 2026:
Estilo 1 — API nativa do Sequelize v7 (sem dependência extra):
Estilo 2 — decorators embutidos do v7 (via @sequelize/core/decorators-legacy, mais próximo do TypeORM):
// models/user.ts — Sequelize v7 com built-in decoratorsimport { Table, Attribute, NotNull, HasMany, Default, CreatedAt, UpdatedAt, PrimaryKey, AutoIncrement,} from '@sequelize/core/decorators-legacy';import { DataTypes, Model } from '@sequelize/core';import { Post } from './post';@Table({ tableName: 'users', timestamps: true })export class User extends Model { @PrimaryKey @AutoIncrement @Attribute(DataTypes.INTEGER) declare id: number; @NotNull @Attribute(DataTypes.STRING(100)) declare name: string; @NotNull @Attribute(DataTypes.STRING) declare email: string; @NotNull @Attribute(DataTypes.STRING) declare passwordHash: string; @Default('user') @Attribute(DataTypes.ENUM('admin', 'user')) declare role: 'admin' | 'user'; // Metadados extras em JSONB — útil no PostgreSQL @Attribute(DataTypes.JSONB) declare metadata: Record<string, unknown> | null; @HasMany(() => Post, 'userId') declare posts: Post[]; @CreatedAt declare createdAt: Date; @UpdatedAt declare updatedAt: Date;}
Tipos de coluna mais usados:
DataTypes
PostgreSQL real
Uso típico
STRING(n)
VARCHAR(n)
Texto curto
TEXT
TEXT
Texto longo
INTEGER
INTEGER
Número inteiro
FLOAT
FLOAT8
Ponto flutuante
DECIMAL(p,s)
NUMERIC(p,s)
Valores monetários
BOOLEAN
BOOLEAN
true/false
DATE
TIMESTAMPTZ
Data + hora com fuso
DATEONLY
DATE
Só data
JSONB
JSONB
Objeto JSON indexável
UUID
UUID
Identificador único
ARRAY(T)
T[]
Array nativo do Postgres
ENUM(...)
ENUM
Conjunto fixo de valores
Associações (relacionamentos)
O Sequelize usa quatro tipos de associação para mapear relacionamentos entre tabelas:
// associations/index.ts — configuração central das associaçõesimport { User } from '../models/user';import { Post } from '../models/post';import { Tag } from '../models/tag';import { PostTag } from '../models/post-tag';import { Profile } from '../models/profile';export function setupAssociations(): void { // 1:1 — User tem exatamente um Profile User.hasOne(Profile, { foreignKey: 'userId', as: 'profile' }); Profile.belongsTo(User, { foreignKey: 'userId', as: 'user' }); // 1:N — User tem muitos Posts User.hasMany(Post, { foreignKey: 'authorId', as: 'posts' }); Post.belongsTo(User, { foreignKey: 'authorId', as: 'author' }); // N:M — Post tem muitas Tags (via tabela pivot PostTag) Post.belongsToMany(Tag, { through: PostTag, foreignKey: 'postId', otherKey: 'tagId', as: 'tags', }); Tag.belongsToMany(Post, { through: PostTag, foreignKey: 'tagId', otherKey: 'postId', as: 'posts', });}
Regra importante: associações são sempre declaradas em pares — HasMany de um lado e BelongsTo do outro. Omitir um lado não gera erro imediato mas causa problemas em include.
Associações polimórficas — o Sequelize não as suporta nativamente de forma elegante. A workaround usual é usar uma coluna resourceType + resourceId, mas isso quebra a integridade referencial no banco. Em 2026, se você precisa de polimorfismo, TypeORM ou uma tabela pivot explícita são alternativas mais limpas. Evite a abordagem polimórfica no Sequelize para código novo.
Queries CRUD
// queries/user-queries.tsimport { Op } from '@sequelize/core';import { User } from '../models/user';import { Post } from '../models/post';// ── LEITURA ──────────────────────────────────────────────────────────────────// Buscar todos (com filtro, ordenação e paginação)const users = await User.findAll({ where: { role: 'user', createdAt: { [Op.gte]: new Date('2025-01-01') }, }, order: [['name', 'ASC']], limit: 20, offset: 0, attributes: ['id', 'name', 'email'], // SELECT parcial});// Buscar um pelo PKconst user = await User.findByPk(42);// Buscar um com condiçãoconst admin = await User.findOne({ where: { email: 'admin@example.com', role: 'admin' },});// Buscar ou criar (retorna [instância, booleano criado])const [newUser, created] = await User.findOrCreate({ where: { email: 'novo@example.com' }, defaults: { name: 'Novo Usuário', passwordHash: '...', role: 'user' },});// ── ESCRITA ───────────────────────────────────────────────────────────────────// Criarconst createdUser = await User.create({ name: 'Maria', email: 'maria@example.com', passwordHash: 'hashed', role: 'user',});// Atualizar (retorna [linhasAfetadas, instânciasAtualizadas])const [count] = await User.update( { role: 'admin' }, { where: { id: 42 } });// Deletarconst deletedCount = await User.destroy({ where: { id: 42 } });// Upsert — insert ou update se já existir (baseado em unique constraints)const [instance, wasCreated] = await User.upsert({ email: 'maria@example.com', name: 'Maria Atualizada', passwordHash: 'novo-hash', role: 'user',});
Operadores (Op) mais usados:
Operador
SQL equivalente
Exemplo
Op.eq
= value
{ age: { [Op.eq]: 18 } }
Op.ne
!= value
{ status: { [Op.ne]: 'deleted' } }
Op.gt / Op.gte
> / >=
{ score: { [Op.gte]: 90 } }
Op.lt / Op.lte
< / <=
{ price: { [Op.lte]: 100 } }
Op.like
LIKE 'pattern'
{ name: { [Op.like]: 'Jo%' } }
Op.iLike
ILIKE (Postgres)
{ name: { [Op.iLike]: '%jose%' } }
Op.in
IN (...)
{ id: { [Op.in]: [1, 2, 3] } }
Op.notIn
NOT IN (...)
{ status: { [Op.notIn]: ['deleted'] } }
Op.between
BETWEEN a AND b
{ age: { [Op.between]: [18, 65] } }
Op.or
OR
{ [Op.or]: [{ a: 1 }, { b: 2 }] }
Op.and
AND
{ [Op.and]: [{ a: 1 }, { b: 2 }] }
Eager loading e include
Eager loading é a técnica de carregar modelos associados junto com a query principal, evitando N+1 queries. No Sequelize, usa-se a opção include.
// eager-loading/post-queries.tsimport { Op } from '@sequelize/core';import { Post } from '../models/post';import { User } from '../models/user';import { Tag } from '../models/tag';import { Comment } from '../models/comment';// Básico: trazer posts com seus autores (LEFT JOIN)const posts = await Post.findAll({ include: [ { model: User, as: 'author', attributes: ['id', 'name', 'email'], // evita trazer passwordHash etc. }, ], order: [['createdAt', 'DESC']], limit: 10,});// Intermediário: filtrar posts que tenham comentários (INNER JOIN implícito)const postsWithComments = await Post.findAll({ include: [ { model: Comment, as: 'comments', required: true, // ← true = INNER JOIN, false = LEFT JOIN (padrão) where: { approved: true }, attributes: ['id', 'body'], }, ],});// Avançado: include aninhado (3 níveis — limite recomendado)const richPosts = await Post.findAll({ include: [ { model: User, as: 'author', attributes: ['id', 'name'], }, { model: Tag, as: 'tags', through: { attributes: [] }, // omite colunas da tabela pivot where: { active: true }, required: false, // LEFT JOIN — posts sem tags também aparecem }, { model: Comment, as: 'comments', required: false, include: [ { model: User, as: 'author', attributes: ['id', 'name'], }, ], }, ],});
required: false vs required: true:
required: false → LEFT JOIN — o registro pai aparece mesmo sem filhos correspondentes
required: true → INNER JOIN — só retorna pais que tenham filhos que satisfaçam o filtro
Omitir required quando há where no include gera um INNER JOIN implícito — esse é um dos bugs mais silenciosos do Sequelize (ver Armadilhas comuns).
Performance com include profundo: 3+ níveis de include geram JOINs em cascata que podem causar produto cartesiano no resultado. Para listas grandes, prefira duas queries separadas e combine em memória, ou use subqueries com { separate: true }.
Hooks são callbacks executados antes ou depois de eventos no ciclo de vida de uma instância. São úteis para lógica transversal sem poluir o código de negócio:
Hooks em Sequelize v7
Os métodos estáticos User.beforeCreate(...), User.afterCreate(...) etc. estão deprecated no Sequelize v7. A forma preferida é declarar os hooks na opção hooks dentro de Model.init():
O exemplo abaixo usa a API legada (ainda funcional em v7, mas que será removida em versões futuras).
// models/user.ts — adicionando hooks ao modelo Userimport bcrypt from 'bcryptjs';import { User } from './user';import { AuditLog } from './audit-log';// Hash de senha antes de criar ou atualizarUser.beforeCreate(async (user) => { if (user.passwordHash) { user.passwordHash = await bcrypt.hash(user.passwordHash, 12); }});User.beforeUpdate(async (user) => { if (user.changed('passwordHash') && user.passwordHash) { user.passwordHash = await bcrypt.hash(user.passwordHash, 12); }});// Audit log após operações críticasUser.afterCreate(async (user, options) => { await AuditLog.create( { action: 'user.created', resourceId: user.id, resourceType: 'User', meta: { email: user.email }, }, { transaction: options.transaction } // ← IMPORTANTE: propagar transação );});User.beforeDestroy(async (user) => { // Soft-delete: arquivar dados antes de remover await AuditLog.create({ action: 'user.deleted', resourceId: user.id, resourceType: 'User', meta: { email: user.email }, });});
Hooks disponíveis:
Evento
Antes
Depois
Criar
beforeCreate, beforeBulkCreate
afterCreate, afterBulkCreate
Atualizar
beforeUpdate, beforeBulkUpdate
afterUpdate, afterBulkUpdate
Deletar
beforeDestroy, beforeBulkDestroy
afterDestroy, afterBulkDestroy
Validar
beforeValidate
afterValidate
Salvar
beforeSave
afterSave
Hooks e transações
Sempre propague a options.transaction para operações dentro de hooks.
Se o hook criar um registro sem a transação, a operação não participa do rollback
e você terá dados inconsistentes no banco mesmo após falha.
Quando usar
O Sequelize ainda faz sentido em cenários específicos:
Projetos legacy com Sequelize v5/v6: migrar para outro ORM tem custo alto (reescrever modelos, queries, migrations, testes) sem benefício claro para código que já funciona. Atualizar para v7 é viável e incremental.
Equipes que já dominam a API: se o time conhece findAll, include, Op e hooks, o custo de troca para Prisma ou Drizzle inclui onboarding real.
Suporte a MSSQL como requisito: o Drizzle tem suporte limitado a SQL Server, e o Prisma tem caveats em features avançadas. O Sequelize é a opção mais madura para Microsoft SQL Server no ecossistema Node.
Migração gradual impossível: se o projeto não pode migrar de ORM de uma vez, o Sequelize v7 permite convivência com código legado.
Quando não usar:
Projeto novo com equipe sem experiência prévia em Sequelize: prefira Prisma (DX superior) ou Drizzle (performance e type safety)
Edge runtimes (Cloudflare Workers, Vercel Edge): o Sequelize não suporta — use Drizzle ou Prisma Accelerate
Times que precisam de type safety rigoroso nas queries sem overhead de configuração
Armadilhas comuns
1. Lazy loading acidental (N+1)
O Sequelize não faz lazy loading automático como alguns ORMs — mas o padrão de buscar instâncias e depois acessar associações em loop gera N+1 manualmente:
// ❌ ERRADO — N+1: 1 query para posts + N queries para cada autorconst posts = await Post.findAll({ limit: 50 });for (const post of posts) { // Cada chamada a getAuthor() dispara uma nova query SELECT const author = await post.getAuthor(); console.log(`${post.title} — ${author.name}`);}
// ✅ CORRETO — eager loading: 1 query com JOIN (ou 2 queries otimizadas)const posts = await Post.findAll({ limit: 50, include: [{ model: User, as: 'author', attributes: ['id', 'name'] }],});for (const post of posts) { // post.author já está carregado, sem queries adicionais console.log(`${post.title} — ${post.author.name}`);}
2. include + where sem required: false gerando INNER JOIN silencioso
// ❌ ERRADO — retorna só posts que TÊM comentários aprovados// (INNER JOIN implícito quando há `where` no include)const posts = await Post.findAll({ include: [ { model: Comment, as: 'comments', where: { approved: true }, // ← gera INNER JOIN sem required explícito }, ],});
// ✅ CORRETO — retorna todos os posts, com ou sem comentários aprovadosconst posts = await Post.findAll({ include: [ { model: Comment, as: 'comments', where: { approved: true }, required: false, // ← LEFT JOIN explícito }, ],});
3. destroy() sem where deletando toda a tabela
// ❌ PERIGO — deleta TODOS os registros da tabela usersawait User.destroy({});// ouawait User.destroy({ where: {} }); // também deleta tudo
// ✅ CORRETO — sempre especifique o critérioawait User.destroy({ where: { id: userId } });// Para operações em massa com intenção explícita, use truncateawait User.truncate(); // deixa claro que é para limpar tudo
Proteção contra destroy() acidental
Configure sequelize.define com { paranoid: true } para soft delete automático
(adiciona deletedAt em vez de deletar fisicamente). Veja a armadilha 5.
4. Op.like com padrão %text% em produção causando full scan
// ❌ PROBLEMA — full table scan em tabelas grandes com padrão prefixado por %const users = await User.findAll({ where: { name: { [Op.like]: `%${searchTerm}%` }, // índice não é usado },});
// ✅ OPÇÃO 1 — padrão sem % no início usa índice em colunas com B-treeconst users = await User.findAll({ where: { name: { [Op.like]: `${searchTerm}%` }, // índice é usado },});// ✅ OPÇÃO 2 — full-text search no PostgreSQL para busca de texto real// sequelize.escape() previne SQL injection ao escapar o valor do usuárioconst users = await User.findAll({ where: sequelize.literal( `to_tsvector('portuguese', name) @@ plainto_tsquery('portuguese', ${sequelize.escape(searchTerm)})` ),});
When to use Sequelize vs alternatives:
Sequelize is the right choice when maintaining a legacy codebase that already uses it — migrating to Prisma or Drizzle carries a real cost in model rewrites, migrations, and team retraining that rarely pays off for stable code. For new projects in 2026, I would default to Prisma for its superior DX and auto-generated type safety, or Drizzle if the team has strong SQL knowledge and needs edge runtime support or minimal runtime overhead.
Eager loading to avoid N+1:
The most common N+1 pattern in Sequelize is fetching a list of records and then calling association methods (like getAuthor()) inside a loop, which fires one query per iteration. The fix is to use include in the original findAll call so the ORM joins the data in a single query (or at most two optimized queries with separate: true). In code review, any loop over Sequelize instances that accesses an association is a N+1 red flag.
The required option in include:
One subtle but critical behavior is that adding a where clause inside include implicitly generates an INNER JOIN, filtering out parent records with no matching children. This surprises developers who expect a LEFT JOIN by default. The explicit required: false should always be set when the intent is to optionally load associations — treating it as optional instead of letting the ORM decide keeps the behavior explicit and reviewable.
Migrations strategy:
In production, I never use sequelize.sync({ force: true }) or even { alter: true } — those are development tools only. The correct approach is Sequelize CLI migrations: each schema change is a versioned migration file committed to git, reviewed in PRs, and applied in a controlled deployment step. Migration files are immutable once merged to main; fixes go in new migration files.