Acelerando o Desenvolvimento: Database Seeding com TypeORM e Faker
Seeds obrigatórios de produção vs seeds de desenvolvimento. Fábrica de entidades com faker-js, idempotência com upsert, ordem de inserção por dependências e integração com o setup de testes.
Um novo desenvolvedor entra na empresa, clona o repositório, sobe o banco de dados com as migrations e... em branco. Para começar a trabalhar na tela de listagem de pedidos, ele precisa criar manualmente: um usuário, um endereço, 10 produtos, 5 pedidos com seus respectivos itens. São 30 minutos perdidos antes de escrever uma linha de código. Multiplique isso por cada reset de banco, cada desenvolvedor novo, cada ambiente de staging.
Database Seeding resolve isso: scripts que populam o banco de dados automaticamente com dados realistas. Mas há uma distinção crucial que muitos projetos ignoram: seeds de produção (dados obrigatórios para o sistema funcionar) e seeds de desenvolvimento (dados fictícios para facilitar o trabalho local). Misturá-los é uma receita para acidente.
Seeds de Produção vs Seeds de Desenvolvimento
- Seeds de Produção — dados obrigatórios: roles do sistema (
ADMIN,USER), permissões iniciais, planos de assinatura, categorias fixas. Rodam no deploy junto com as migrations. Devem ser idempotentes (podem rodar múltiplas vezes sem duplicar dados). - Seeds de Desenvolvimento — dados fictícios: 100 usuários, 50 produtos, 200 pedidos. Rodam apenas localmente ou em ambientes de staging. Nunca devem rodar em produção.
Estrutura de Pastas
src/infra/typeorm/
├── migrations/ # Estrutura do banco (rodam em produção)
├── seeds/
│ ├── production/ # Seeds obrigatórios — idempotentes, rodam em produção
│ │ ├── 01-roles.seed.ts
│ │ ├── 02-permissions.seed.ts
│ │ └── index.ts # Orquestra a execução em ordem
│ └── development/ # Seeds de desenvolvimento — apenas local/staging
│ ├── factories/
│ │ ├── UserFactory.ts
│ │ ├── ProductFactory.ts
│ │ └── OrderFactory.ts
│ └── index.tsSeeds de Produção: Idempotência com Upsert
A idempotência garante que rodar o seed múltiplas vezes não duplica dados. Use upsert do TypeORM (INSERT OR UPDATE) com a coluna única como critério de conflito:
import type { DataSource } from 'typeorm';
import { RoleModel } from '../../models/RoleModel';
const ROLES = [
{ id: '00000000-0000-0000-0000-000000000001', name: 'ADMIN', description: 'Acesso total ao sistema' },
{ id: '00000000-0000-0000-0000-000000000002', name: 'MANAGER', description: 'Gerencia usuários e relatórios' },
{ id: '00000000-0000-0000-0000-000000000003', name: 'USER', description: 'Usuário padrão' },
];
export async function seedRoles(dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(RoleModel);
// upsert: insere se não existe, atualiza se existir pelo conflito em 'name'
// IDs fixos (UUIDs determinísticos) garantem que as FKs nunca quebram
await repo.upsert(ROLES, {
conflictPaths: ['name'],
skipUpdateIfNoValuesChanged: true, // Não dispara update desnecessário
});
console.log(`✅ Roles: ${ROLES.length} registros.`);
}import { dataSource } from '../../dataSource';
import { seedRoles } from './01-roles.seed';
import { seedPermissions } from './02-permissions.seed';
async function runProductionSeeds(): Promise<void> {
if (!dataSource.isInitialized) {
await dataSource.initialize();
}
console.log('🌱 Iniciando seeds de produção...');
// Ordem importa: permissões dependem de roles
await seedRoles(dataSource);
await seedPermissions(dataSource);
console.log('✅ Seeds de produção concluídos.');
await dataSource.destroy();
}
runProductionSeeds().catch((err) => {
console.error('❌ Erro nos seeds:', err);
process.exit(1);
});Factory de Entidades com @faker-js/faker
O faker-js localizado em pt_BR gera nomes, endereços, e-mails e CPFs em formato brasileiro — tornando os dados muito mais reais para demonstrações. A senha fixa senha@123 para todos os usuários de seed é intencional: facilita o acesso durante desenvolvimento sem precisar de reset de senha. O método createMany usando Promise.all com save em batch é muito mais eficiente do que chamar create em loop — uma única transação no banco ao invés de N transações individuais.
npm install --save-dev @faker-js/fakerimport { faker } from '@faker-js/faker/locale/pt_BR'; // Faker localizado em PT-BR
import { hash } from 'bcrypt';
import type { DataSource } from 'typeorm';
import { UserModel } from '../../../models/UserModel';
interface CreateUserOptions {
override?: Partial<UserModel>;
count?: number;
}
export class UserFactory {
constructor(private dataSource: DataSource) {}
private async build(override?: Partial<UserModel>): Promise<Partial<UserModel>> {
// Senha fixa para todos os usuários de seed — facilita testes manuais
const passwordHash = await hash('senha@123', 10);
return {
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email().toLowerCase(),
passwordHash,
emailVerified: true,
avatar: faker.image.avatar(),
createdAt: faker.date.past({ years: 2 }),
...override, // Permite sobrescrever campos específicos
};
}
// Cria um único usuário
async create(override?: Partial<UserModel>): Promise<UserModel> {
const data = await this.build(override);
return this.dataSource.getRepository(UserModel).save(data);
}
// Cria múltiplos usuários em batch (muito mais eficiente)
async createMany(count: number, override?: Partial<UserModel>): Promise<Array<UserModel>> {
const users = await Promise.all(
Array.from({ length: count }, () => this.build(override))
);
return this.dataSource.getRepository(UserModel).save(users);
}
}import { faker } from '@faker-js/faker/locale/pt_BR';
import type { DataSource } from 'typeorm';
import { OrderModel } from '../../../models/OrderModel';
import type { UserModel } from '../../../models/UserModel';
export class OrderFactory {
constructor(private dataSource: DataSource) {}
async createMany(count: number, users: Array<UserModel>): Promise<Array<OrderModel>> {
const orders = Array.from({ length: count }, () => ({
id: faker.string.uuid(),
// Distribui aleatoriamente entre os usuários passados
customerId: faker.helpers.arrayElement(users).id,
status: faker.helpers.arrayElement(['PENDING', 'PAID', 'SHIPPED', 'DELIVERED', 'CANCELED']),
total: Number(faker.commerce.price({ min: 50, max: 5000 })),
createdAt: faker.date.past({ years: 1 }),
}));
return this.dataSource.getRepository(OrderModel).save(orders);
}
}Orquestrando o Seed de Desenvolvimento
import { dataSource } from '../../dataSource';
import { UserFactory } from './factories/UserFactory';
import { OrderFactory } from './factories/OrderFactory';
async function runDevelopmentSeeds(): Promise<void> {
// Proteção: nunca roda em produção
if (process.env.NODE_ENV === 'production') {
console.error('❌ Seeds de desenvolvimento não podem rodar em produção!');
process.exit(1);
}
await dataSource.initialize();
const userFactory = new UserFactory(dataSource);
const orderFactory = new OrderFactory(dataSource);
console.log('🌱 Criando dados de desenvolvimento...');
// Cria usuário admin com credenciais fixas conhecidas
await userFactory.create({
name: 'Admin Dev',
email: 'admin@dev.local',
emailVerified: true,
});
// Cria 50 usuários aleatórios
const users = await userFactory.createMany(50);
console.log(`✅ ${users.length + 1} usuários criados.`);
// Cria 200 pedidos distribuídos entre os usuários
const orders = await orderFactory.createMany(200, users);
console.log(`✅ ${orders.length} pedidos criados.`);
console.log('🎉 Seed de desenvolvimento concluído!');
await dataSource.destroy();
}
runDevelopmentSeeds().catch((err) => {
console.error('❌ Erro:', err);
process.exit(1);
});Scripts no package.json
{
"scripts": {
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli",
"migration:run": "pnpm typeorm migration:run -- -d src/infra/typeorm/dataSource.ts",
"seed:prod": "ts-node src/infra/typeorm/seeds/production/index.ts",
"seed:dev": "ts-node src/infra/typeorm/seeds/development/index.ts",
"db:setup": "pnpm migration:run && pnpm seed:prod",
"db:setup:dev": "pnpm db:setup && pnpm seed:dev"
}
}
// Onboarding de um novo dev: apenas 'pnpm db:setup:dev'
// Primeiro acesso de produção: apenas 'pnpm db:setup'Seeds em Testes: beforeAll e afterAll
As factories de entidades também são úteis em testes de integração — reutilize-as no beforeAll para criar o estado inicial dos testes:
describe('Orders API (E2E)', () => {
let app: Express;
let userFactory: UserFactory;
let testUser: UserModel;
beforeAll(async () => {
await testDataSource.initialize();
userFactory = new UserFactory(testDataSource);
// Cria usuário com credenciais fixas para o teste
testUser = await userFactory.create({
email: 'test-orders@example.com',
});
});
afterAll(async () => {
// Limpa os dados criados para o teste
await testDataSource.getRepository(UserModel).delete({ id: testUser.id });
await testDataSource.destroy();
});
it('deve listar os pedidos do usuário', async () => {
const response = await request(app)
.get('/api/orders')
.set('Authorization', `Bearer ${generateTestToken(testUser.id)}`);
expect(response.status).toBe(200);
});
});Conclusão
Seeding bem estruturado é parte da infraestrutura do projeto, não um script descartável. A separação entre seeds obrigatórios (idempotentes, rodam em produção) e seeds de desenvolvimento (Faker, apenas local) é a distinção mais importante. Com factories tipadas e reutilizáveis em testes, você elimina duplicação de código entre o seed de desenvolvimento e os testes de integração.