⚡ AutomationsAI|Portal de Cursos →

Verificando acesso...

MÓDULO 5.1

⚙️ Stack e setup

A fundação técnica do clone Postiz: NestJS, Prisma, BullMQ, monorepo com pnpm e configuração via variáveis de ambiente validadas com zod.

6
Tópicos
35
Minutos
Avançado
Nível
Prático
Tipo
1

🆚 NestJS vs Express

O Postiz original usa NestJS — e não por moda. Quando o app cresce para 8+ módulos (auth, posts, integrations, billing, queue, etc.), Express vira espaguete: você inventa sua própria estrutura, seu próprio DI, sua própria forma de testar. Nest entrega tudo isso pronto: decorators, injeção de dependência e módulos são primeira classe.

Compare a mesma rota nos dois frameworks. No Express, o controller carrega serviço, validação e resposta tudo junto. No Nest, cada coisa no seu lugar — e o PostsService é injetado automaticamente pelo container DI.

// apps/backend/src/posts/posts.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';

@Controller('posts')
export class PostsController {
  constructor(private readonly posts: PostsService) {}

  @Get()
  list() {
    return this.posts.findAll();
  }

  @Post()
  create(@Body() dto: CreatePostDto) {
    return this.posts.create(dto);
  }

  @Get(':id')
  byId(@Param('id') id: string) {
    return this.posts.findOne(id);
  }
}

💡 Dica prática

Nest é opinionado de propósito. Não tente "expressar" um projeto Nest — abrace o padrão Module/Controller/Service. Quando você inventar uma terceira camada porque "fica mais limpo", está perdendo justamente o motivo de ter escolhido Nest.

Conceitos-chave

DI Container

Resolve dependências de classes automaticamente.

Decorators

@Controller, @Get, @Injectable dão semântica.

Módulos

Unidade de organização — importa, exporta, encapsula.

Pipes/Guards

Validação e auth como middlewares declarativos.

2

🗄️ Prisma + PostgreSQL

Prisma resolve três dores do TypeScript com SQL: tipagem ponta-a-ponta (o client é gerado a partir do schema.prisma), migrations versionadas e autocomplete em queries. Esqueça raw SQL para 90% dos casos.

O schema.prisma é fonte única da verdade do modelo de dados. Alterou um campo? prisma migrate dev gera a migration SQL e regenera o client.

// apps/backend/prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  password  String
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id          String   @id @default(cuid())
  content     String
  scheduledAt DateTime?
  publishedAt DateTime?
  status      String   @default("draft")
  authorId    String
  author      User     @relation(fields: [authorId], references: [id])

  @@index([authorId, status])
}
# Init + primeira migration
pnpm dlx prisma init --datasource-provider postgresql
pnpm dlx prisma migrate dev --name init
pnpm dlx prisma generate

Conceitos-chave

schema.prisma

DSL declarativa do modelo de dados.

migrate dev

Gera SQL versionado em prisma/migrations/.

Client gerado

Tipos TypeScript a partir do schema.

Prisma Studio

GUI web para inspecionar e editar dados.

3

📬 BullMQ + Redis

Agendamento de posts não pode viver no processo HTTP. Se o request termina antes do horário, o post some. A solução são filas: BullMQ guarda o job no Redis com delay, e um worker separado processa quando chega a hora.

Três peças: Queue (quem produz jobs), Worker (quem consome) e QueueEvents (quem observa). Worker pode rodar em outro processo, outro container, outra máquina.

// apps/backend/src/queue/publish.queue.ts
import { Queue, Worker } from 'bullmq';
import IORedis from 'ioredis';

const connection = new IORedis(process.env.REDIS_URL!, {
  maxRetriesPerRequest: null,
});

export const publishQueue = new Queue('publish', { connection });

// Agendar post para daqui a 1 hora
await publishQueue.add(
  'publish-post',
  { postId: 'cm123', provider: 'twitter' },
  {
    delay: 60 * 60 * 1000,
    attempts: 5,
    backoff: { type: 'exponential', delay: 2000 },
    removeOnComplete: 1000,
    removeOnFail: 5000,
  }
);

// Worker (processo separado em produção)
new Worker(
  'publish',
  async (job) => {
    const { postId, provider } = job.data;
    return await publishToProvider(postId, provider);
  },
  { connection, concurrency: 10 }
);

Dica prática

Sempre configure attempts e backoff: exponential. APIs sociais retornam rate-limit (429) com frequência — sem retry exponencial, você queima a janela inteira em 3 tentativas e perde o post.

✓ O que FAZER

  • Worker em processo separado (apps/worker).
  • removeOnComplete/removeOnFail para não inchar o Redis.
  • Idempotência: jobs podem rodar 2x — não duplique o post.
  • Monitorar com Bull Board ou QueueEvents.

✗ O que NÃO fazer

  • Worker no mesmo processo HTTP em produção.
  • Esquecer maxRetriesPerRequest: null no ioredis.
  • Jobs sem attempts — uma falha e o post some.
  • Guardar payload gigante no job — passe só o ID.

Conceitos-chave

Delayed job

Job que só executa após delay ms.

Backoff

Espera crescente entre tentativas.

Concurrency

Jobs simultâneos por worker.

DLQ

Jobs falhados ficam para inspeção manual.

4

📁 Estrutura de pastas monorepo

Backend, frontend e worker no mesmo repo, com código compartilhado em libs/. Tipos do Prisma, DTOs e validação zod ficam em um pacote consumido pelos três apps — sem duplicação, sem desincronização.

mkblogs/
├── apps/
│   ├── backend/              # NestJS API
│   │   ├── src/
│   │   │   ├── auth/
│   │   │   ├── posts/
│   │   │   ├── integrations/
│   │   │   └── main.ts
│   │   ├── prisma/
│   │   │   ├── schema.prisma
│   │   │   └── migrations/
│   │   └── package.json
│   ├── frontend/             # Next.js
│   │   ├── src/
│   │   │   ├── app/
│   │   │   └── components/
│   │   └── package.json
│   └── worker/               # BullMQ workers
│       ├── src/
│       │   └── publish.worker.ts
│       └── package.json
├── libs/
│   └── shared/               # tipos + zod schemas compartilhados
│       ├── src/
│       │   ├── dto/
│       │   └── env.schema.ts
│       └── package.json
├── pnpm-workspace.yaml
├── .env.example
└── package.json

📊 Por que três apps?

  • apps/backend: API HTTP. Não toca em filas — só enfileira.
  • apps/worker: processo que consome filas. Escala separado do HTTP.
  • apps/frontend: Next.js. Fala só com o backend via REST/tRPC.
  • libs/shared: DTOs, tipos, schemas zod. Importado por todos.

Conceitos-chave

Monorepo

Vários pacotes, um repositório git.

apps vs libs

Apps rodam; libs são consumidas por apps.

Workspace

Resolve deps locais sem publicar no npm.

Tipos cruzados

Backend e frontend importam o mesmo DTO.

5

📦 pnpm workspaces

pnpm é o gerenciador certo para monorepo: instala dependências uma vez e linka via symlinks (node_modules 5x menor que npm), entende workspace:* nativamente e é mais rápido. Use npm ou yarn aqui só se for masoquista.

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'libs/*'
// package.json (raiz)
{
  "name": "mkblogs",
  "private": true,
  "scripts": {
    "dev:backend": "pnpm --filter backend dev",
    "dev:frontend": "pnpm --filter frontend dev",
    "dev:worker": "pnpm --filter worker dev",
    "dev": "pnpm -r --parallel dev",
    "build": "pnpm -r build",
    "lint": "pnpm -r lint"
  },
  "devDependencies": {
    "typescript": "^5.5.0",
    "tsx": "^4.16.0"
  }
}
// apps/backend/package.json (trecho)
{
  "name": "backend",
  "dependencies": {
    "@nestjs/core": "^10.4.0",
    "@nestjs/common": "^10.4.0",
    "@prisma/client": "^5.18.0",
    "bullmq": "^5.12.0",
    "ioredis": "^5.4.0",
    "zod": "^3.23.0",
    "@mkblogs/shared": "workspace:*"
  }
}
# Comandos úteis
pnpm install                          # instala tudo do workspace
pnpm --filter backend add @nestjs/jwt # adiciona dep só no backend
pnpm --filter worker dev              # roda script de um pacote
pnpm -r build                         # build recursivo em todos

Conceitos-chave

workspace:*

Aponta para versão local do pacote.

--filter

Executa em pacotes específicos.

-r (recursivo)

Roda o mesmo script em todos os pacotes.

Content-addressable

Cada versão de pacote é armazenada uma vez.

6

🔐 Variáveis de ambiente

Config via .env não é suficiente. Sem validação, um typo em DATABASE_URL só estoura no primeiro request em produção. A solução: validar com zod no boot — se algo está errado, o app nem sobe.

// libs/shared/src/env.schema.ts
import { z } from 'zod';

export const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  PORT: z.coerce.number().int().positive().default(3000),

  DATABASE_URL: z.string().url(),
  REDIS_URL: z.string().url(),

  JWT_SECRET: z.string().min(32, 'JWT_SECRET precisa de 32+ chars'),
  JWT_EXPIRES_IN: z.string().default('7d'),

  FRONTEND_URL: z.string().url(),

  // OAuth providers (opcionais no dev)
  TWITTER_CLIENT_ID: z.string().optional(),
  TWITTER_CLIENT_SECRET: z.string().optional(),
});

export type Env = z.infer<typeof envSchema>;

export function loadEnv(): Env {
  const parsed = envSchema.safeParse(process.env);
  if (!parsed.success) {
    console.error('❌ .env inválido:', parsed.error.flatten().fieldErrors);
    process.exit(1);
  }
  return parsed.data;
}
# .env.example (commitado, sem segredos)
NODE_ENV=development
PORT=3000

DATABASE_URL=postgresql://postgres:postgres@localhost:5432/mkblogs
REDIS_URL=redis://localhost:6379

JWT_SECRET=  # gere com: openssl rand -hex 32
JWT_EXPIRES_IN=7d

FRONTEND_URL=http://localhost:3001

TWITTER_CLIENT_ID=
TWITTER_CLIENT_SECRET=
// apps/backend/src/main.ts
import { loadEnv } from '@mkblogs/shared';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

const env = loadEnv(); // valida ANTES de subir o Nest

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(env.PORT);
}
bootstrap();

🛡️ Dica prática

Valide o env antes do NestFactory.create. Se o schema falhar, o processo termina com exit code 1 e o orquestrador (Docker/Kubernetes) detecta — vale muito mais que descobrir o erro 30 min depois quando um usuário tenta logar.

Conceitos-chave

zod

Validação com inferência de tipos TS.

.env.example

Contrato versionado de configuração.

coerce

Converte string do env em número/bool.

Fail fast

Erro no boot > erro em runtime.

🎯 Resumo do Módulo

NestJS escolhido com critério — DI, decorators e módulos resolvem a complexidade que Express empurra para o dev.
Prisma + PostgreSQL — schema declarativo, migrations versionadas e client tipado fim-a-fim.
BullMQ + Redis — jobs delayed com retry exponencial, worker em processo separado.
Monorepo organizadoapps/backend, apps/frontend, apps/worker e libs/shared.
pnpm workspacesworkspace:*, --filter e -r dominam o fluxo diário.
Env validado com zod — schema único, .env.example commitado, fail fast no boot.

Próximo Módulo:

5.2 — Auth, modelos de dados e primeiras rotas