⚡ AutomationsAI|Portal de Cursos →

Verificando acesso...

TRILHA 5

🛠️ Construindo do Zero

Aprenda a arquitetar e construir um agendador multi-rede do zero. Stack NestJS + Prisma + PostgreSQL + BullMQ + Redis, padrão Provider para abstrair redes sociais e fila com retry inteligente.

5
Módulos
30
Tópicos
~4h
Duração
Avançado
Nível

Mapa da trilha

Conteúdo detalhado

5.1 ~35 min

⚙️ Stack e setup

As escolhas de stack que vão te perseguir por anos. NestJS, Prisma, BullMQ e monorepo com pnpm.

O que é:

NestJS é um framework Node.js opinativo, com arquitetura modular (controllers, services, modules), DI nativa e suporte first-class a TypeScript. Express é minimalista — você monta tudo.

Por que aprender:

Para um agendador multi-rede com workers, providers e jobs, NestJS impõe estrutura que evita virar espaguete em 3 meses. Decorators (@Cron, @Process), guards e interceptors já vêm prontos.

Conceitos-chave:

DI container, módulos, providers (injectables), pipes/guards, integração nativa com BullMQ via @nestjs/bullmq.

O que é:

Prisma é um ORM/query builder que gera client TypeScript a partir do schema.prisma. Sintaxe declarativa, autocomplete real, migrations versionadas. Postgres é o banco relacional padrão para SaaS.

Por que aprender:

Postar em N redes envolve relações (User → Post → PostTarget → SocialAccount). Prisma resolve isso com includes tipados — você não escreve JOIN à mão e o TS pega erro antes de runtime.

Conceitos-chave:

schema.prisma, prisma generate, prisma migrate dev/deploy, client.$transaction, includes vs select.

O que é:

BullMQ é uma fila de jobs em Redis: você enfileira um job com payload + delay, e um worker em outro processo consome quando chega a hora. Suporta retry, backoff, prioridade.

Por que aprender:

Sem fila, você teria que rodar cron checando "tem post para agora?" a cada minuto. BullMQ resolve com delay nativo: enfileirou com delay=2h, o job só dispara em 2h. E sobrevive a restart.

Conceitos-chave:

Queue, Worker, Job, delay, attempts, backoff exponencial, removeOnComplete, QueueEvents.

O que é:

Monorepo: um repo com múltiplos pacotes/apps. Layout clássico: apps/api (NestJS), apps/worker (BullMQ), apps/web (Next.js), packages/db (Prisma client), packages/types (tipos compartilhados).

Por que aprender:

Você precisa compartilhar tipos entre API e front, e o Prisma client entre API e worker. Sem monorepo, vira duplicação e drift de tipos. Com monorepo, refatora num lugar só.

Conceitos-chave:

workspace, internal packages, hoisting de deps, turborepo (opcional), import "@mkblogs/db".

O que é:

pnpm é alternativa ao npm/yarn com content-addressable store: deps ficam num único local, hard-linked no node_modules. workspaces define pacotes locais via pnpm-workspace.yaml.

Por que aprender:

Em monorepo node_modules cresce rápido. pnpm economiza GBs, instala mais rápido e bloqueia phantom dependencies (deps que você usa mas não declarou).

Conceitos-chave:

pnpm-workspace.yaml, pnpm --filter api dev, deps internas com workspace:*, store global em ~/.local/share/pnpm.

O que é:

DATABASE_URL, REDIS_URL, JWT_SECRET, S3_BUCKET, ENCRYPTION_KEY... Variáveis de ambiente carregadas via dotenv ou @nestjs/config, validadas com Zod ou class-validator.

Por que aprender:

Subir produção e descobrir que esqueceu de setar ENCRYPTION_KEY é o pesadelo clássico. Validação no boot do app evita: se variável obrigatória faltou, app não sobe.

Conceitos-chave:

.env.example commitado, .env no .gitignore, ConfigModule.forRoot({ validationSchema }), separar segredos do app config.

Ver Completo
5.2 ~40 min

🗄️ Schema do banco

O modelo de dados que aguenta 1 post → N redes sem virar caos. Tokens, mídia e migrations.

O que é:

Entidade central: id (cuid), email único, passwordHash (bcrypt), createdAt, plan (free/pro). Relação 1:N com SocialAccount e Post.

Por que aprender:

Toda autorização gira em torno de User. Antes de buscar um post, você confirma user.id == post.userId. Sem isso, qualquer um vê posts de qualquer um.

Conceitos-chave:

cuid vs uuid vs autoincrement, índice em email, never expose passwordHash em DTO, soft delete vs hard delete.

O que é:

Conta conectada de uma rede: userId, provider (bluesky, x, mastodon), handle, accessToken, refreshToken, expiresAt. Tokens armazenados criptografados (AES-256-GCM).

Por que aprender:

Token vazado é acesso à conta do usuário. Banco vazado sem encryption = LGPD/multa + reputação destruída. Encryption-at-rest é não-negociável.

Conceitos-chave:

ENCRYPTION_KEY em env, encrypt/decrypt helpers, unique (userId, provider, handle), refresh antes de expirar.

O que é:

O conteúdo "canônico": id, userId, text, scheduledAt, status (draft/scheduled/publishing/published/failed). Relação N:N com Media via tabela pivot. 1:N com PostTarget.

Por que aprender:

Separar o "conteúdo desejado" (Post) do "execução por rede" (PostTarget) permite postar mesma coisa em 5 redes e ter status independente de cada uma. Bluesky publicou, X falhou — sem perder o histórico.

Conceitos-chave:

enum PostStatus, índice em scheduledAt, índice composto (userId, status), text como TEXT (não VARCHAR).

O que é:

Cada "destino" de um post: postId, socialAccountId, status, providerPostId (URI do post na rede), publishedAt, errorMessage, attemptCount.

Por que aprender:

É aqui que mora a verdade do que rolou em cada rede. Sem PostTarget, você não sabe se o post chegou no X — só se "tentou postar". Com PostTarget, dá pra mostrar links clicáveis pro usuário.

Conceitos-chave:

providerPostId guardado após sucesso, status independente por target, retry só do target que falhou, índice (postId, socialAccountId) único.

O que é:

Arquivo de mídia anexado: id, userId, key (path no S3), url (CDN), mimeType, sizeBytes, width, height. Banco guarda metadados; arquivo bruto vive no S3/R2.

Por que aprender:

Guardar imagens no Postgres como bytea é receita de banco lento. S3 é projetado pra isso: URLs públicas/assinadas, CDN na frente, custo baixo.

Conceitos-chave:

key (não URL completa) no banco, presigned URL pra upload direto, mime sniffing no backend, cleanup de órfãs.

O que é:

prisma migrate dev cria um SQL versionado em prisma/migrations/ a partir das mudanças no schema.prisma. prisma migrate deploy aplica em produção.

Por que aprender:

Sem migrations, mudar uma coluna em prod é manual e propenso a erro. Com migrations versionadas, o histórico fica no Git e o deploy aplica na ordem certa.

Conceitos-chave:

migrate dev vs deploy, never edit applied migration, prisma db push para protótipo, backup antes de prod.

Ver Completo
5.3 ~50 min

🔌 Padrão Provider

A abstração que faz seu código não saber a diferença entre Bluesky, X, Mastodon ou Threads.

O que é:

Contrato TS que toda rede social implementa: authenticate(credentials), refreshToken(token), post(text, media), deletePost(id). Tipos de entrada/saída fixos, implementação varia.

Por que aprender:

É o coração da extensibilidade. Adicionar Mastodon vira criar um arquivo novo que implementa a interface — zero mudança no resto do app.

Conceitos-chave:

interface vs abstract class, Result type pra erros, normalização de respostas (sempre retorna { uri, publishedAt }).

O que é:

ProviderFactory.get(provider: 'bluesky' | 'x') retorna a instância certa. Pode ser um Map registrado no boot ou um decorator @Injectable do Nest.

Por que aprender:

Sem factory, você cai num switch gigante toda vez que precisa postar. Com factory, só pega o provider pelo nome e chama .post(). Limpo e testável.

Conceitos-chave:

DI no NestJS, Map<string, SocialProvider>, erro claro quando provider não existe.

O que é:

SDK oficial @atproto/api. Login com identifier + app password, agent.post({ text, embed }) para publicar, agent.uploadBlob para mídia.

Por que aprender:

Bluesky é a rede mais aberta hoje, sem rate limits agressivos pra apps pequenos e tem app password (não precisa OAuth completo). Bom primeiro provider para implementar.

Conceitos-chave:

AtpAgent, app passwords, BskyAgent.post(), embed images via uploadBlob, parsing de URI at://...

O que é:

X (Twitter) API v2 com OAuth 2.0 PKCE. POST /2/tweets com bearer token. Upload de mídia ainda via v1.1 (/media/upload). Plano básico custa USD 100/mês.

Por que aprender:

É a maior dor de cabeça (OAuth, refresh com rotação, custo, rate limits). Implementar bem aqui prova que sua arquitetura aguenta provider chato. Os outros são moleza depois.

Conceitos-chave:

OAuth 2.0 PKCE, refresh token rotation, media_id em upload v1.1, rate limit 429 + Retry-After.

O que é:

Classes de erro de domínio: RefreshTokenError (token expirou e refresh falhou), RateLimitError (com retryAfter), ProviderTransientError (rede), ProviderPermanentError (conteúdo inválido).

Por que aprender:

Sem classes de erro, o worker não sabe se deve retry ou abandonar. Retry de erro permanente = job preso. Não-retry de erro transiente = falha boba.

Conceitos-chave:

extends Error, instanceof no worker pra decidir, mensagem amigável pro usuário ("reconecte sua conta"), notificar via email se reconnect necessário.

O que é:

Para cada provider, teste cobre: post() de sucesso retorna URI, post() com 429 lança RateLimitError, refresh() que falha lança RefreshTokenError. Mockar @atproto/api e fetch.

Por que aprender:

Provider quebrar em produção é constrangedor. Teste unitário pega regressão antes do deploy. Vitest + msw (mock service worker) é o combo padrão.

Conceitos-chave:

vi.mock(), MSW para HTTP, golden file de payload, cobertura mínima 80% nos providers críticos.

Ver Completo
5.4 ~40 min

⏰ Fila e agendamento

BullMQ com delay nativo, retry exponencial, DLQ e quando migrar para Temporal.

O que é:

3 primitivas: Queue (produtor enfileira), Worker (consumidor processa), Job (a unidade com payload + opções). Toda comunicação roda no Redis via lua scripts atômicos.

Por que aprender:

É o estado-da-arte de filas em Node hoje. Sucessor maduro do Bull, com tipagem boa e API limpa. Pronto pra produção em milhares de empresas.

Conceitos-chave:

queue.add(name, data, opts), new Worker(queueName, fn), concurrency, prefix de keys por env.

O que é:

queue.add('publish-post', { postTargetId }, { delay: scheduledAt - now }). BullMQ guarda no Redis e só move o job pra "active" quando o delay vence.

Por que aprender:

Resolve agendamento sem cron. "Postar dia 25 às 14h" vira um delay de N ms — preciso, escalável e sobrevive a restart porque está no Redis.

Conceitos-chave:

delay em ms, jobId determinístico para idempotência, queue.getDelayed(), removeOnComplete: { age: 7d }.

O que é:

Função async (job) => { busca PostTarget no banco → ProviderFactory.get(provider).post(...) → grava providerPostId no PostTarget → marca status = published }.

Por que aprender:

É o ponto onde fila + banco + provider se encontram. Erro aqui = post não chega ou chega duplicado. Idempotência é vital: checar status antes de postar de novo.

Conceitos-chave:

@Processor do @nestjs/bullmq, transaction pra atualizar status, idempotência via providerPostId, log com correlationId.

O que é:

attempts: 5, backoff: { type: 'exponential', delay: 5000 }. Falhas transientes (rede, 5xx) reentram a fila com delay crescente (5s, 10s, 20s, 40s, 80s).

Por que aprender:

Sem retry, qualquer hiccup do X deixa post como failed. Com backoff exponencial, a API tem espaço pra se recuperar e não você não bombardeia ela durante outage.

Conceitos-chave:

só fazer retry de erro transiente, throw vs Unrecoverable error, attempts.attemptsMade, log de cada tentativa.

O que é:

Após todos os attempts esgotados, mover o job para uma "failed queue" separada. Alertar dev, marcar PostTarget como failed, enviar email ao usuário.

Por que aprender:

Sem DLQ, falhas terminais somem no console. Com DLQ, você tem dashboard de "o que deu ruim e precisa investigar" e pode reprocessar manualmente quando consertar bug.

Conceitos-chave:

QueueEvents 'failed', failedQueue.add(jobData), Bull Board ou Arena UI, alerta no Slack via webhook.

O que é:

Temporal.io: orquestrador de workflows com estado durável, replay automático, sagas multi-step. Substitui BullMQ quando você precisa de coreografia complexa entre passos.

Por que aprender:

BullMQ é ótimo para jobs isolados. Quando seu fluxo vira "se A falhar depois de B sucesso, desfazer B" — você precisa de workflows com compensação. Temporal nasceu pra isso.

Conceitos-chave:

workflow vs activity, replay determinístico, sinais, Temporal Cloud vs self-hosted, custo: BullMQ < Temporal.

Ver Completo
5.5 ~35 min

🖼️ Upload de mídia

Multer, S3/R2, sharp para resize e limpeza de órfãs. Mídia certa, sem inflar storage.

O que é:

Multer parseia multipart/form-data de uploads HTTP. NestJS tem integração via @nestjs/platform-express com @UseInterceptors(FileInterceptor('file')).

Por que aprender:

JSON não carrega binário sem base64 (33% overhead). multipart é o padrão da web para upload. Sem entender, você acaba enviando arquivo como base64 e estourando body limit.

Conceitos-chave:

memory storage vs disk storage, fileSize limit, dest temp folder, file.buffer no controller.

O que é:

@aws-sdk/client-s3 com PutObjectCommand. Cloudflare R2 é compatível com a mesma API e tem egress grátis — ideal para servir mídia pública.

Por que aprender:

No S3, USD ~0.023/GB/mês + custo de egress. No R2, mesma coisa de storage mas zero de egress. Para um app que serve imagens pra usuários, R2 sai 5-10x mais barato.

Conceitos-chave:

endpoint customizado (R2), key com prefixo userId/, ACL public-read vs presigned, region.

O que é:

getSignedUrl(client, GetObjectCommand, { expiresIn: 3600 }) gera URL temporária assinada. Cliente baixa direto do S3, sem passar pelo seu servidor.

Por que aprender:

Sem presigned, ou você deixa bucket público (vazamento) ou faz proxy no servidor (banda cara). Presigned: temporário + seguro + servido direto pela infra do S3/R2.

Conceitos-chave:

expiresIn em segundos, presigned PUT pra upload direto do browser, cache de URL no Redis.

O que é:

sharp é lib Node baseada em libvips: resize, format webp, strip metadata, gerar thumbnails. Roda direto em buffer ou stream, sem subprocess.

Por que aprender:

Usuário envia foto de 12MB do celular. Bluesky aceita 1MB. Sem resize, post falha. Com sharp, redimensiona, comprime para webp e envia 200KB de qualidade visual idêntica.

Conceitos-chave:

sharp(buffer).resize(2048).webp({ quality: 80 }).toBuffer(), strip exif, generate thumbnail 300x300.

O que é:

Não confiar no Content-Type do header. Usar file-type para detectar pelos magic bytes do buffer. Limit fileSize no Multer + double-check no serviço.

Por que aprender:

Aceitar "image/jpeg" pelo header é receita de RCE: usuário envia .exe renomeado. Magic bytes não mentem. Sem validação real, vira vetor de upload malicioso.

Conceitos-chave:

file-type lib, allowlist de mimes (image/jpeg, image/png, image/webp), maxSize 10MB, 413 Payload Too Large.

O que é:

Job diário (BullMQ repeat) que busca Media sem postId associado e mais antigas que 24h, deleta do S3 e do banco. Mídia órfã = upload iniciado, post nunca finalizado.

Por que aprender:

Sem cleanup, storage cresce indefinidamente. Em 6 meses você paga storage de coisa que ninguém vê. Cron simples resolve, mas precisa cuidar de race com upload em andamento.

Conceitos-chave:

queue.add com repeat: { cron: '0 3 * * *' }, soft delete antes de hard delete, S3 lifecycle policy como fallback.

Ver Completo
← Trilha 4: Blogs Trilha 6: Deploy →