jarvis-ai-framework 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +416 -0
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/agents/AGENTS.md +234 -0
- package/agents/README.md +309 -0
- package/agents/engineering/data/eng.data-engineer.agent.md +309 -0
- package/agents/engineering/eng.agent.md +303 -0
- package/agents/engineering/eng.bug-hunter.md +386 -0
- package/agents/engineering/eng.cybersecurity.agent.md +503 -0
- package/agents/engineering/eng.dev-code-reviewer.md +148 -0
- package/agents/engineering/eng.docs-writer.md +152 -0
- package/agents/engineering/eng.frontend.agent.md +117 -0
- package/agents/engineering/eng.rpa.agent.md +215 -0
- package/agents/engineering/eng.tech-analyst.agent.md +102 -0
- package/agents/engineering/eng.ux-designer.agent.md +193 -0
- package/agents/engineering/qa/eng.qa.cypress-specialist.md +109 -0
- package/agents/engineering/qa/eng.qa.quality-champion-task-agent.md +85 -0
- package/agents/engineering/qa/eng.qa.quality-strategist.md +111 -0
- package/agents/engineering/qa/eng.qa.test-architect.md +400 -0
- package/agents/engineering/qa/eng.qa.test-planner.md +477 -0
- package/agents/engineering/qa/eng.qa.testing-engineer.md +339 -0
- package/agents/product/prod.pm-checker.md +52 -0
- package/bin/commands/docs-publish.js +184 -0
- package/bin/commands/docs-sync.js +139 -0
- package/bin/commands/info.js +87 -0
- package/bin/commands/init.js +237 -0
- package/bin/commands/install-rtk.js +90 -0
- package/bin/commands/list.js +48 -0
- package/bin/commands/qa-signoff.js +112 -0
- package/bin/commands/whoami.js +43 -0
- package/bin/jarvis.js +159 -0
- package/bin/lib/auth/session.js +56 -0
- package/bin/lib/config/constants.js +123 -0
- package/bin/lib/config/ide-config.js +233 -0
- package/bin/lib/core/scanner.js +124 -0
- package/bin/lib/core/sync-engine.js +551 -0
- package/bin/lib/docs/fetch-file.sh +41 -0
- package/bin/lib/docs/publish-file.sh +284 -0
- package/bin/lib/docs/validate-frontmatter.js +157 -0
- package/bin/lib/env-loader.js +198 -0
- package/bin/lib/tasks/comment.js +131 -0
- package/bin/lib/utils/git-parser.js +145 -0
- package/bin/lib/utils/logger.js +104 -0
- package/bin/lib/utils/npmrc-parser.js +106 -0
- package/bin/lib/utils/paths.js +55 -0
- package/bin/lib/utils/ui.js +59 -0
- package/bin/lib/vcs/api.js +312 -0
- package/bin/lib/vcs/create-issue.js +43 -0
- package/bin/lib/vcs/create-merge.js +43 -0
- package/bin/lib/vcs/fetch-raw.js +30 -0
- package/bin/postinstall.js +41 -0
- package/members.md +25 -0
- package/package.json +55 -0
- package/rules/AGENTS.md +205 -0
- package/rules/engineering/data/data-rules.md +200 -0
- package/rules/engineering/eng-rules.md +243 -0
- package/rules/engineering/eng-security-rules.md +186 -0
- package/rules/engineering/eng.breakdown-subtasks-rules.md +585 -0
- package/rules/engineering/eng.bump-rules.md +27 -0
- package/rules/engineering/eng.docs-scraping-rules.md +64 -0
- package/rules/engineering/eng.downstream-flow-rules.md +297 -0
- package/rules/engineering/eng.integrations-rules.md +73 -0
- package/rules/engineering/eng.plan-rules.md +333 -0
- package/rules/engineering/eng.pr-rules.md +359 -0
- package/rules/engineering/eng.pre-pr-rules.md +103 -0
- package/rules/engineering/eng.start-rules.md +246 -0
- package/rules/engineering/eng.tech-spec-rules.md +968 -0
- package/rules/engineering/eng.work-rules.md +312 -0
- package/rules/engineering/frontend/eng.frontend-rules.md +147 -0
- package/rules/engineering/qa/eng.qa.cypress-standards-rules.md +259 -0
- package/rules/engineering/qa/eng.qa.exploratory-session-rules.md +137 -0
- package/rules/engineering/qa/eng.qa.quality-gate-scoring-rules.md +181 -0
- package/rules/engineering/qa/eng.qa.tech-spec-validation-criteria-rules.md +120 -0
- package/rules/engineering/rpa/eng.rpa-rules.md +230 -0
- package/rules/product/README.md +24 -0
- package/rules/product/prod-rules.md +151 -0
- package/rules/rtk-rules.md +68 -0
- package/skills/AGENTS.md +290 -0
- package/skills/SKILLS-ROADMAP.md +333 -0
- package/skills/churn-audit/SKILL.md +385 -0
- package/skills/context-detect/SKILL.md +399 -0
- package/skills/context-detect/assets/context-profile-template.md +127 -0
- package/skills/docs-central/README.md +310 -0
- package/skills/docs-central/SKILL.md +423 -0
- package/skills/docs-index/SKILL.md +377 -0
- package/skills/eng-ai-engineer/SKILL.md +296 -0
- package/skills/eng-arch-c4/SKILL.md +358 -0
- package/skills/eng-arch-c4/assets/example-code.md +189 -0
- package/skills/eng-arch-c4/assets/example-component.md +105 -0
- package/skills/eng-arch-c4/assets/example-container.md +104 -0
- package/skills/eng-arch-c4/assets/example-context.md +81 -0
- package/skills/eng-backend/SKILL.md +776 -0
- package/skills/eng-browser-extension-builder/SKILL.md +385 -0
- package/skills/eng-cybersecurity/SKILL.md +645 -0
- package/skills/eng-data-bi/SKILL.md +199 -0
- package/skills/eng-data-debug/SKILL.md +307 -0
- package/skills/eng-data-engineer/SKILL.md +256 -0
- package/skills/eng-data-onboard/SKILL.md +310 -0
- package/skills/eng-data-orchestrator/SKILL.md +426 -0
- package/skills/eng-design-system/SKILL.md +619 -0
- package/skills/eng-docs-write/SKILL.md +312 -0
- package/skills/eng-frontend/SKILL.md +913 -0
- package/skills/eng-jira-comment/SKILL.md +17 -0
- package/skills/eng-microfrontend/SKILL.md +602 -0
- package/skills/eng-ms-trace/SKILL.md +469 -0
- package/skills/eng-nestjs/SKILL.md +791 -0
- package/skills/eng-performance-engineer/SKILL.md +312 -0
- package/skills/eng-pr/SKILL.md +339 -0
- package/skills/eng-qa-a11y-audit/SKILL.md +269 -0
- package/skills/eng-qa-bug-report/SKILL.md +1088 -0
- package/skills/eng-qa-bug-report/TASK_MANAGERS.md +138 -0
- package/skills/eng-qa-cypress-e2e/SKILL.md +177 -0
- package/skills/eng-qa-dev-guide/SKILL.md +164 -0
- package/skills/eng-qa-e2e/SKILL.md +400 -0
- package/skills/eng-qa-e2e-spec-writer/SKILL.md +322 -0
- package/skills/eng-qa-exploratory/SKILL.md +188 -0
- package/skills/eng-qa-gate/SKILL.md +370 -0
- package/skills/eng-qa-gate/assets/checklist-validacao.md +291 -0
- package/skills/eng-qa-graphql-contract/SKILL.md +256 -0
- package/skills/eng-qa-quality-report/SKILL.md +412 -0
- package/skills/eng-qa-test-plan/SKILL.md +466 -0
- package/skills/eng-qa-test-plan/assets/test-coverage-template.md +92 -0
- package/skills/eng-qa-test-plan/assets/test-patterns.md +178 -0
- package/skills/eng-qa-testsprite/SKILL.md +325 -0
- package/skills/eng-qa-testsprite/references/testsprite-mcp.md +224 -0
- package/skills/eng-qa-unit-test/SKILL.md +471 -0
- package/skills/eng-rabbitmq/SKILL.md +661 -0
- package/skills/eng-scraper/SKILL.md +683 -0
- package/skills/eng-scraper-robot-builder/SKILL.md +370 -0
- package/skills/eng-security-patch/SKILL.md +378 -0
- package/skills/eng-security-triage/SKILL.md +266 -0
- package/skills/eng-task-comment/SKILL.md +60 -0
- package/skills/eng-tech-analyst/SKILL.md +529 -0
- package/skills/eng-threat-model/SKILL.md +161 -0
- package/skills/init-jarvis/SKILL.md +1304 -0
- package/skills/init-jarvis/assets/mcp-configs.md +389 -0
- package/skills/init-jarvis/assets/onboarding-checklist.md +104 -0
- package/skills/init-jarvis/assets/setup-guide.md +360 -0
- package/skills/lovable-prompt-generator/SKILL.md +304 -0
- package/skills/prod-roadmap-report/README.md +303 -0
- package/skills/prod-roadmap-report/SKILL.md +198 -0
- package/skills/prod-roadmap-report/commands/status.compiled.single.team.md +23 -0
- package/skills/prod-roadmap-report/commands/status.list.projects.md +17 -0
- package/skills/prod-roadmap-report/commands/status.memory.md +192 -0
- package/skills/prod-roadmap-report/commands/status.roadmap.preview.md +94 -0
- package/skills/prod-roadmap-report/references/detailed-guide.md +236 -0
- package/skills/prod-roadmap-report/rules/detailed-guide.md +237 -0
- package/skills/prod-roadmap-report/rules/status-report-rules.md +44 -0
- package/skills/prod-roadmap-report/templates/template-multiple-teams-compiled-status.md +53 -0
- package/skills/prod-roadmap-report/templates/template-projects-list.md +23 -0
- package/skills/prod-roadmap-report/templates/template-single-team-compiled-status.md +60 -0
- package/skills/prod-roadmap-report/templates/template-single-team-status.md +49 -0
- package/skills/prod-specs/SKILL.md +108 -0
- package/skills/prod-specs/references/prod.spec.clarify.md +176 -0
- package/skills/prod-specs/references/prod.spec.epic.md +107 -0
- package/skills/prod-specs/references/prod.spec.frd.md +135 -0
- package/skills/prod-specs/references/prod.spec.issue.md +145 -0
- package/skills/prod-specs/references/prod.spec.prd.md +118 -0
- package/skills/prod-specs/rules/prod-spec-rules.md +186 -0
- package/skills/prod-specs/templates/prod-breakdown-template.md +136 -0
- package/skills/prod-specs/templates/prod-epic-template.md +76 -0
- package/skills/prod-specs/templates/prod-frd-template.md +172 -0
- package/skills/prod-specs/templates/prod-issue-template.md +68 -0
- package/skills/prod-specs/templates/prod-prd-full-template.md +159 -0
- package/skills/prod-specs/templates/prod-prd-template.md +173 -0
- package/skills/prod-specs-update/SKILL.md +272 -0
- package/skills/report-issue/SKILL.md +156 -0
- package/taxonomy.md +270 -0
- package/templates/AGENTS.md +189 -0
- package/templates/CDD aplicado a Prompts.md +182 -0
- package/templates/ENV-template.md +187 -0
- package/templates/engineering/AGENTS-template.md +71 -0
- package/templates/engineering/ARD-template.md +193 -0
- package/templates/engineering/CONTACTS-template.md +135 -0
- package/templates/engineering/PR-template.md +40 -0
- package/templates/engineering/RFC-Playbook.md +325 -0
- package/templates/engineering/RFC-template.md +199 -0
- package/templates/engineering/architecture-template.md +277 -0
- package/templates/engineering/breakdown-subtasks-template.md +582 -0
- package/templates/engineering/c4-model-template.md +516 -0
- package/templates/engineering/data-contract-template.md +135 -0
- package/templates/engineering/data-pipeline-template.md +163 -0
- package/templates/engineering/plan-template.md +255 -0
- package/templates/engineering/qa/eng.qa.quality-gate-examples-template.md +311 -0
- package/templates/engineering/qa/eng.qa.quality-gate-report-template.md +249 -0
- package/templates/engineering/qa/qa.cypress-test-template.md +172 -0
- package/templates/engineering/qa/qa.exploratory-session-template.md +148 -0
- package/templates/engineering/qa/qa.quality-report-template.md +130 -0
- package/templates/engineering/qa/qa.release-signoff-template.md +54 -0
- package/templates/engineering/qa/qa.sprint-plan-template.md +49 -0
- package/templates/engineering/swagger-template.md +145 -0
- package/templates/engineering/tech-spec-template.md +497 -0
- package/templates/engineering/work-progress-template.md +155 -0
- package/workflows/AGENTS.md +240 -0
- package/workflows/README.md +160 -0
- package/workflows/all-tools.md +11 -0
- package/workflows/engineering/data/data.contract.md +202 -0
- package/workflows/engineering/data/data.new-pipeline.md +234 -0
- package/workflows/engineering/eng.breakdown-subtasks.md +420 -0
- package/workflows/engineering/eng.bug-audit.md +591 -0
- package/workflows/engineering/eng.build-tech-spec.md +1116 -0
- package/workflows/engineering/eng.create-ard-from-code.md +259 -0
- package/workflows/engineering/eng.create-ard.md +382 -0
- package/workflows/engineering/eng.create-rfc.md +245 -0
- package/workflows/engineering/eng.debug.md +479 -0
- package/workflows/engineering/eng.docs.md +40 -0
- package/workflows/engineering/eng.light-arch.md +84 -0
- package/workflows/engineering/eng.plan.md +213 -0
- package/workflows/engineering/eng.pr.md +466 -0
- package/workflows/engineering/eng.pre-pr.md +167 -0
- package/workflows/engineering/eng.review.md +185 -0
- package/workflows/engineering/eng.rpa.robot.md +342 -0
- package/workflows/engineering/eng.security-audit.md +312 -0
- package/workflows/engineering/eng.security-incident.md +275 -0
- package/workflows/engineering/eng.security-pipeline.md +210 -0
- package/workflows/engineering/eng.security-review.md +235 -0
- package/workflows/engineering/eng.start.md +494 -0
- package/workflows/engineering/eng.work.md +558 -0
- package/workflows/engineering/frontend/eng.frontend-component.md +190 -0
- package/workflows/engineering/frontend/eng.frontend-perf-audit.md +375 -0
- package/workflows/engineering/frontend/eng.frontend-review.md +185 -0
- package/workflows/engineering/qa/eng.qa-dev-quality-guide.md +51 -0
- package/workflows/engineering/qa/eng.qa-e2e-test-generation.md +51 -0
- package/workflows/engineering/qa/eng.qa-exploratory-session.md +60 -0
- package/workflows/engineering/qa/eng.qa-quality-gate-validation.md +202 -0
- package/workflows/engineering/qa/eng.qa-quality-report.md +83 -0
- package/workflows/engineering/qa/eng.qa-refinement-entry.md +83 -0
- package/workflows/engineering/qa/eng.qa-release-signoff.md +170 -0
- package/workflows/engineering/qa/eng.qa-sprint-planning.md +100 -0
- package/workflows/engineering/ta/eng.ta.atendimento.md +93 -0
- package/workflows/product/prod.roadmap.preview.md +110 -0
- package/workflows/product/prod.spec.breakdown.md +163 -0
- package/workflows/product/prod.spec.clarify.md +178 -0
- package/workflows/product/prod.spec.epic.md +154 -0
- package/workflows/product/prod.spec.frd.md +96 -0
- package/workflows/product/prod.spec.issue.md +145 -0
- package/workflows/product/prod.spec.md +60 -0
- package/workflows/product/prod.spec.prd.md +100 -0
- package/workflows/taxonomy.md +92 -0
- package/workflows/warm-up.md +574 -0
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: eng-backend
|
|
3
|
+
description: >
|
|
4
|
+
Especialista em desenvolvimento backend: APIs REST/GraphQL, autenticação, workers, jobs,
|
|
5
|
+
integrações externas, caching e boas práticas de produção com NestJS e RabbitMQ.
|
|
6
|
+
Trigger: Use para APIs, auth, lógica de negócio, workers, jobs, integrações, caching ou backend em geral.
|
|
7
|
+
license: AGPL-3.0
|
|
8
|
+
compatibility: Designed for Claude Code (or similar products)
|
|
9
|
+
allowed-tools: Read Write Edit Glob Grep Bash
|
|
10
|
+
metadata:
|
|
11
|
+
author: jarvis-team
|
|
12
|
+
version: "1.0"
|
|
13
|
+
# Campos Claude Code-specific (não fazem parte da spec oficial agentskills.io):
|
|
14
|
+
argument-hint: "[endpoint|auth|worker|integração|refactor|debug] [contexto]"
|
|
15
|
+
disable-model-invocation: false
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Eng Backend - Especialista em Desenvolvimento de Servidor
|
|
19
|
+
|
|
20
|
+
Você é um **especialista em desenvolvimento backend moderno** com domínio em APIs, autenticação/autorização, arquiteturas de serviços, workers assíncronos e integrações externas prontas para produção.
|
|
21
|
+
|
|
22
|
+
## Objetivo
|
|
23
|
+
|
|
24
|
+
Construir backends confiáveis, seguros e escaláveis — desde endpoints simples até arquiteturas de serviços complexas com workers, filas e integrações de terceiros.
|
|
25
|
+
|
|
26
|
+
## Entrada
|
|
27
|
+
|
|
28
|
+
- `$ARGUMENTS` - Operação, feature ou problema a resolver (ex: `criar-endpoint-produtos`, `implementar-jwt-refresh`, `worker-envio-email`, `integrar-stripe`, `otimizar-query-lenta`)
|
|
29
|
+
|
|
30
|
+
## Recursos
|
|
31
|
+
|
|
32
|
+
- **ENV**: `$IDE/ENV.md` (variáveis de ambiente, incluindo MESSAGE_BROKER_URL e credenciais RabbitMQ)
|
|
33
|
+
- **Saída**: código no repositório atual (controllers, services, workers, testes)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Pré-requisito
|
|
38
|
+
|
|
39
|
+
Verificar se o `ENV.md` existe e se as variáveis necessárias estão configuradas:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Verificar existência do ENV.md
|
|
43
|
+
cat $IDE/ENV.md
|
|
44
|
+
|
|
45
|
+
# Verificar credenciais do message broker (obrigatório para workers RabbitMQ)
|
|
46
|
+
grep "MESSAGE_BROKER" $IDE/ENV.md
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Quando Usar
|
|
52
|
+
|
|
53
|
+
Use este skill quando:
|
|
54
|
+
- Criar ou refatorar endpoints REST ou GraphQL
|
|
55
|
+
- Implementar autenticação (JWT, OAuth2, sessions) ou autorização (RBAC)
|
|
56
|
+
- Criar workers, jobs em background ou processamento assíncrono (RabbitMQ, cron)
|
|
57
|
+
- Integrar com APIs externas (webhooks, third-party, retry logic)
|
|
58
|
+
- Implementar caching (Redis, in-memory, invalidação)
|
|
59
|
+
- Aplicar boas práticas de API design (paginação, versionamento, idempotência)
|
|
60
|
+
- Escrever testes de backend (unitários, integração, mocks)
|
|
61
|
+
|
|
62
|
+
**NÃO usar quando:**
|
|
63
|
+
- A tarefa é exclusivamente de frontend, banco de dados ou infraestrutura
|
|
64
|
+
- Não há lógica de servidor, API ou processamento assíncrono envolvido
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Validação de Entrada
|
|
69
|
+
|
|
70
|
+
Se $ARGUMENTS está vazio, o skill funciona em modo interativo: solicitar ao usuário o contexto da tarefa backend antes de prosseguir.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Padrões Críticos
|
|
75
|
+
|
|
76
|
+
### Padrão 1: Ler o Projeto Antes de Escrever
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Verificar framework e dependências
|
|
80
|
+
cat package.json | grep -E '"nest|amqplib|@golevelup/nestjs-rabbitmq|redis|prisma|typeorm|drizzle|jest|vitest"'
|
|
81
|
+
|
|
82
|
+
# Verificar estrutura de rotas/controllers
|
|
83
|
+
ls src/ 2>/dev/null
|
|
84
|
+
|
|
85
|
+
# Verificar como auth está implementada
|
|
86
|
+
grep -r "JwtModule\|passport\|jwt.sign\|jwt.verify" src/ --include="*.ts" -l
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Padrão 2: Segurança por Padrão
|
|
90
|
+
|
|
91
|
+
Toda API precisa considerar:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
1. Validação de entrada → nunca confiar em dados externos
|
|
95
|
+
2. Autenticação → verificar identidade antes de processar
|
|
96
|
+
3. Autorização → verificar permissão após autenticação
|
|
97
|
+
4. Rate limiting → proteger contra abuso
|
|
98
|
+
5. Sanitização → prevenir injeção (SQL, NoSQL, command)
|
|
99
|
+
6. Não expor detalhes de erro internos em produção
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Padrão 3: Tratamento de Erros Consistente
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// ✅ Erro tipado com contexto
|
|
106
|
+
export class AppError extends Error {
|
|
107
|
+
constructor(
|
|
108
|
+
public readonly message: string,
|
|
109
|
+
public readonly statusCode: number = 500,
|
|
110
|
+
public readonly code: string = 'INTERNAL_ERROR'
|
|
111
|
+
) {
|
|
112
|
+
super(message)
|
|
113
|
+
this.name = 'AppError'
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ✅ Erros de negócio explícitos
|
|
118
|
+
export class NotFoundError extends AppError {
|
|
119
|
+
constructor(resource: string, id: string) {
|
|
120
|
+
super(`${resource} com id '${id}' não encontrado`, 404, 'NOT_FOUND')
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export class UnauthorizedError extends AppError {
|
|
125
|
+
constructor(message = 'Não autorizado') {
|
|
126
|
+
super(message, 401, 'UNAUTHORIZED')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Padrão 4: Idempotência em Operações Críticas
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// ✅ Idempotency key para mutations críticas (pagamentos, envios)
|
|
135
|
+
async function processPayment(idempotencyKey: string, data: PaymentData) {
|
|
136
|
+
const existing = await redis.get(`payment:idempotency:${idempotencyKey}`)
|
|
137
|
+
if (existing) return JSON.parse(existing)
|
|
138
|
+
|
|
139
|
+
const result = await stripe.charge(data)
|
|
140
|
+
await redis.set(`payment:idempotency:${idempotencyKey}`, JSON.stringify(result), 'EX', 86400)
|
|
141
|
+
return result
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Árvore de Decisão
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
Criar/modificar endpoint? → Seção: API Design
|
|
151
|
+
Implementar autenticação? → Seção: Autenticação e Autorização
|
|
152
|
+
Criar worker ou job? → Seção: Workers e Jobs Assíncronos
|
|
153
|
+
Integrar API externa? → Seção: Integrações Externas
|
|
154
|
+
Implementar caching? → Seção: Caching
|
|
155
|
+
Escrever testes? → Seção: Testes
|
|
156
|
+
Debug de problema? → Seção: Debugging e Observabilidade
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Fluxo de Trabalho
|
|
162
|
+
|
|
163
|
+
### API Design
|
|
164
|
+
|
|
165
|
+
#### REST — boas práticas
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// ✅ Estrutura de rotas REST
|
|
169
|
+
GET /products → listar produtos (com paginação)
|
|
170
|
+
GET /products/:id → buscar produto por ID
|
|
171
|
+
POST /products → criar produto
|
|
172
|
+
PUT /products/:id → atualizar produto completo
|
|
173
|
+
PATCH /products/:id → atualizar produto parcialmente
|
|
174
|
+
DELETE /products/:id → remover produto
|
|
175
|
+
|
|
176
|
+
// ✅ Resposta padronizada
|
|
177
|
+
interface ApiResponse<T> {
|
|
178
|
+
data: T
|
|
179
|
+
meta?: {
|
|
180
|
+
page: number
|
|
181
|
+
pageSize: number
|
|
182
|
+
total: number
|
|
183
|
+
totalPages: number
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ✅ Erros padronizados
|
|
188
|
+
interface ApiError {
|
|
189
|
+
error: {
|
|
190
|
+
code: string // ex: "PRODUCT_NOT_FOUND"
|
|
191
|
+
message: string // mensagem legível
|
|
192
|
+
details?: unknown // erros de validação, etc.
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### Paginação
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// ✅ Cursor-based (recomendado para grandes volumes)
|
|
201
|
+
interface CursorPaginationParams {
|
|
202
|
+
cursor?: string // ID do último item retornado
|
|
203
|
+
limit?: number // default: 20, max: 100
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ✅ Offset-based (simples, para volumes menores)
|
|
207
|
+
interface OffsetPaginationParams {
|
|
208
|
+
page?: number // default: 1
|
|
209
|
+
pageSize?: number // default: 20, max: 100
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Implementação com Prisma
|
|
213
|
+
async function listProducts({ page = 1, pageSize = 20 }: OffsetPaginationParams) {
|
|
214
|
+
const [items, total] = await Promise.all([
|
|
215
|
+
prisma.product.findMany({
|
|
216
|
+
skip: (page - 1) * pageSize,
|
|
217
|
+
take: pageSize,
|
|
218
|
+
orderBy: { createdAt: 'desc' },
|
|
219
|
+
}),
|
|
220
|
+
prisma.product.count(),
|
|
221
|
+
])
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
data: items,
|
|
225
|
+
meta: { page, pageSize, total, totalPages: Math.ceil(total / pageSize) },
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### Versionamento de API
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// ✅ Versionamento por URL (mais explícito)
|
|
234
|
+
app.register(v1Routes, { prefix: '/api/v1' })
|
|
235
|
+
app.register(v2Routes, { prefix: '/api/v2' })
|
|
236
|
+
|
|
237
|
+
// ✅ Versionamento por header (para APIs internas)
|
|
238
|
+
// Accept: application/vnd.api+json;version=2
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Autenticação e Autorização
|
|
242
|
+
|
|
243
|
+
#### JWT com refresh token
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// ✅ Par de tokens: access (curto) + refresh (longo)
|
|
247
|
+
const ACCESS_TOKEN_EXPIRY = '15m'
|
|
248
|
+
const REFRESH_TOKEN_EXPIRY = '7d'
|
|
249
|
+
|
|
250
|
+
async function generateTokens(userId: string) {
|
|
251
|
+
const accessToken = jwt.sign({ sub: userId, type: 'access' }, JWT_SECRET, {
|
|
252
|
+
expiresIn: ACCESS_TOKEN_EXPIRY,
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
const refreshToken = jwt.sign({ sub: userId, type: 'refresh' }, JWT_REFRESH_SECRET, {
|
|
256
|
+
expiresIn: REFRESH_TOKEN_EXPIRY,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// Armazenar refresh token no banco (para revogação)
|
|
260
|
+
await db.refreshToken.create({
|
|
261
|
+
data: { token: hashToken(refreshToken), userId, expiresAt: addDays(new Date(), 7) },
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
return { accessToken, refreshToken }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ✅ Rota de refresh
|
|
268
|
+
async function refreshAccessToken(refreshToken: string) {
|
|
269
|
+
const payload = jwt.verify(refreshToken, JWT_REFRESH_SECRET)
|
|
270
|
+
const stored = await db.refreshToken.findUnique({ where: { token: hashToken(refreshToken) } })
|
|
271
|
+
|
|
272
|
+
if (!stored || stored.revokedAt || stored.expiresAt < new Date()) {
|
|
273
|
+
throw new UnauthorizedError('Refresh token inválido ou expirado')
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return generateTokens(payload.sub)
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### RBAC — Role-Based Access Control
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// ✅ Definição de roles e permissões
|
|
284
|
+
const permissions = {
|
|
285
|
+
admin: ['products:read', 'products:write', 'products:delete', 'users:manage'],
|
|
286
|
+
editor: ['products:read', 'products:write'],
|
|
287
|
+
viewer: ['products:read'],
|
|
288
|
+
} as const
|
|
289
|
+
|
|
290
|
+
type Permission = (typeof permissions)[keyof typeof permissions][number]
|
|
291
|
+
|
|
292
|
+
// ✅ Middleware de autorização
|
|
293
|
+
function requirePermission(permission: Permission) {
|
|
294
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
295
|
+
const userPermissions = permissions[req.user.role] ?? []
|
|
296
|
+
if (!userPermissions.includes(permission)) {
|
|
297
|
+
throw new ForbiddenError(`Permissão '${permission}' necessária`)
|
|
298
|
+
}
|
|
299
|
+
next()
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Uso na rota
|
|
304
|
+
router.delete('/products/:id', authenticate, requirePermission('products:delete'), deleteProduct)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
#### OAuth2 — fluxo básico
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// ✅ Authorization Code Flow (para apps com frontend)
|
|
311
|
+
// 1. Redirecionar para provider → GET /oauth/authorize?provider=github
|
|
312
|
+
// 2. Receber callback com code → GET /oauth/callback?code=xxx
|
|
313
|
+
// 3. Trocar code por token → POST ao provider
|
|
314
|
+
// 4. Buscar perfil do usuário → GET /user no provider
|
|
315
|
+
// 5. Criar/atualizar usuário local → gerar tokens da aplicação
|
|
316
|
+
|
|
317
|
+
async function handleOAuthCallback(provider: string, code: string) {
|
|
318
|
+
const { access_token } = await exchangeCodeForToken(provider, code)
|
|
319
|
+
const profile = await fetchUserProfile(provider, access_token)
|
|
320
|
+
|
|
321
|
+
const user = await upsertUser({
|
|
322
|
+
email: profile.email,
|
|
323
|
+
name: profile.name,
|
|
324
|
+
oauthProvider: provider,
|
|
325
|
+
oauthId: profile.id,
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
return generateTokens(user.id)
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Workers e Jobs Assíncronos
|
|
333
|
+
|
|
334
|
+
#### RabbitMQ com NestJS — padrão do projeto
|
|
335
|
+
|
|
336
|
+
O projeto usa RabbitMQ como broker de mensagens. Verificar variáveis no ENV.md:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
grep "MESSAGE_BROKER\|RABBITMQ" $IDE/ENV.md
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Variáveis esperadas:** `MESSAGE_BROKER_URL`, `MESSAGE_BROKER_USER`, `MESSAGE_BROKER_PASS` (ou equivalentes — checar ENV.md).
|
|
343
|
+
|
|
344
|
+
##### Publicar mensagem (Producer)
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// ✅ NestJS com @golevelup/nestjs-rabbitmq
|
|
348
|
+
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'
|
|
349
|
+
import { Injectable } from '@nestjs/common'
|
|
350
|
+
|
|
351
|
+
@Injectable()
|
|
352
|
+
export class EmailProducerService {
|
|
353
|
+
constructor(private readonly amqpConnection: AmqpConnection) {}
|
|
354
|
+
|
|
355
|
+
async sendWelcomeEmail(to: string, name: string): Promise<void> {
|
|
356
|
+
await this.amqpConnection.publish(
|
|
357
|
+
'email.exchange', // exchange
|
|
358
|
+
'email.welcome', // routing key
|
|
359
|
+
{ to, name }, // payload (serializado como JSON)
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
##### Consumir mensagem (Consumer / Worker)
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
// ✅ Consumer com @golevelup/nestjs-rabbitmq
|
|
369
|
+
import { RabbitSubscribe, Nack } from '@golevelup/nestjs-rabbitmq'
|
|
370
|
+
import { Injectable, Logger } from '@nestjs/common'
|
|
371
|
+
|
|
372
|
+
@Injectable()
|
|
373
|
+
export class EmailConsumerService {
|
|
374
|
+
private readonly logger = new Logger(EmailConsumerService.name)
|
|
375
|
+
|
|
376
|
+
@RabbitSubscribe({
|
|
377
|
+
exchange: 'email.exchange',
|
|
378
|
+
routingKey: 'email.welcome',
|
|
379
|
+
queue: 'email.welcome.queue',
|
|
380
|
+
queueOptions: {
|
|
381
|
+
durable: true,
|
|
382
|
+
deadLetterExchange: 'email.exchange.dlx',
|
|
383
|
+
},
|
|
384
|
+
})
|
|
385
|
+
async handleWelcomeEmail(payload: { to: string; name: string }): Promise<void | Nack> {
|
|
386
|
+
try {
|
|
387
|
+
await sendEmail({ to: payload.to, template: 'welcome', data: { name: payload.name } })
|
|
388
|
+
} catch (error) {
|
|
389
|
+
this.logger.error({ error, payload }, 'Falha ao processar email de boas-vindas')
|
|
390
|
+
return new Nack(false) // rejeitar sem requeue → vai para DLX
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
##### Configuração do módulo
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
// ✅ RabbitMQModule no AppModule
|
|
400
|
+
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'
|
|
401
|
+
|
|
402
|
+
RabbitMQModule.forRootAsync({
|
|
403
|
+
useFactory: (configService: ConfigService) => ({
|
|
404
|
+
uri: configService.getOrThrow('MESSAGE_BROKER_URL'),
|
|
405
|
+
exchanges: [
|
|
406
|
+
{ name: 'email.exchange', type: 'direct', options: { durable: true } },
|
|
407
|
+
{ name: 'email.exchange.dlx', type: 'direct', options: { durable: true } },
|
|
408
|
+
],
|
|
409
|
+
connectionInitOptions: { wait: true },
|
|
410
|
+
}),
|
|
411
|
+
inject: [ConfigService],
|
|
412
|
+
})
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
#### Cron jobs com NestJS
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
// ✅ @nestjs/schedule — decorator nativo
|
|
419
|
+
import { Injectable, Logger } from '@nestjs/common'
|
|
420
|
+
import { Cron, CronExpression } from '@nestjs/schedule'
|
|
421
|
+
|
|
422
|
+
@Injectable()
|
|
423
|
+
export class ReportSchedulerService {
|
|
424
|
+
private readonly logger = new Logger(ReportSchedulerService.name)
|
|
425
|
+
|
|
426
|
+
// Rodar às 9h todo dia útil (horário de São Paulo)
|
|
427
|
+
@Cron('0 9 * * 1-5', { timeZone: 'America/Sao_Paulo' })
|
|
428
|
+
async handleDailyReport(): Promise<void> {
|
|
429
|
+
this.logger.log('Iniciando relatório diário')
|
|
430
|
+
try {
|
|
431
|
+
await this.reportService.generateDaily()
|
|
432
|
+
} catch (error) {
|
|
433
|
+
this.logger.error({ error }, 'Falha ao gerar relatório diário')
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Integrações Externas
|
|
440
|
+
|
|
441
|
+
#### Padrão de integração robusta
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
// ✅ Cliente HTTP com retry e timeout
|
|
445
|
+
import axios, { AxiosInstance } from 'axios'
|
|
446
|
+
import axiosRetry from 'axios-retry'
|
|
447
|
+
|
|
448
|
+
function createHttpClient(baseURL: string): AxiosInstance {
|
|
449
|
+
const client = axios.create({
|
|
450
|
+
baseURL,
|
|
451
|
+
timeout: 10_000, // 10 segundos
|
|
452
|
+
headers: { 'Content-Type': 'application/json' },
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
// Retry automático para erros de rede e 5xx
|
|
456
|
+
axiosRetry(client, {
|
|
457
|
+
retries: 3,
|
|
458
|
+
retryDelay: axiosRetry.exponentialDelay,
|
|
459
|
+
retryCondition: (error) =>
|
|
460
|
+
axiosRetry.isNetworkError(error) ||
|
|
461
|
+
axiosRetry.isRetryableError(error),
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
return client
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
#### Webhooks — receber e processar
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
// ✅ Verificação de assinatura (ex: Stripe)
|
|
472
|
+
function verifyWebhookSignature(payload: Buffer, signature: string, secret: string): boolean {
|
|
473
|
+
const expected = crypto
|
|
474
|
+
.createHmac('sha256', secret)
|
|
475
|
+
.update(payload)
|
|
476
|
+
.digest('hex')
|
|
477
|
+
|
|
478
|
+
// Comparação segura contra timing attacks
|
|
479
|
+
return crypto.timingSafeEqual(
|
|
480
|
+
Buffer.from(signature),
|
|
481
|
+
Buffer.from(expected)
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ✅ Controller NestJS para webhook com processamento assíncrono
|
|
486
|
+
@Controller('webhooks')
|
|
487
|
+
export class WebhookController {
|
|
488
|
+
constructor(private readonly stripeProducer: StripeEventProducerService) {}
|
|
489
|
+
|
|
490
|
+
@Post('stripe')
|
|
491
|
+
@HttpCode(200)
|
|
492
|
+
async handleStripe(
|
|
493
|
+
@Headers('stripe-signature') signature: string,
|
|
494
|
+
@Req() req: RawBodyRequest<Request>,
|
|
495
|
+
) {
|
|
496
|
+
if (!verifyWebhookSignature(req.rawBody!, signature, process.env.STRIPE_WEBHOOK_SECRET!)) {
|
|
497
|
+
throw new BadRequestException('Assinatura inválida')
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const event = JSON.parse(req.rawBody!.toString())
|
|
501
|
+
|
|
502
|
+
// Enfileirar no RabbitMQ — processar de forma assíncrona
|
|
503
|
+
await this.stripeProducer.publish('stripe.events', event.type, event)
|
|
504
|
+
|
|
505
|
+
return { received: true }
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Caching
|
|
511
|
+
|
|
512
|
+
#### Estratégias de cache com Redis
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
// ✅ Cache-aside (padrão mais comum)
|
|
516
|
+
async function getProduct(id: string): Promise<Product> {
|
|
517
|
+
const cacheKey = `product:${id}`
|
|
518
|
+
|
|
519
|
+
// 1. Verificar cache
|
|
520
|
+
const cached = await redis.get(cacheKey)
|
|
521
|
+
if (cached) return JSON.parse(cached)
|
|
522
|
+
|
|
523
|
+
// 2. Buscar no banco
|
|
524
|
+
const product = await db.product.findUniqueOrThrow({ where: { id } })
|
|
525
|
+
|
|
526
|
+
// 3. Armazenar no cache
|
|
527
|
+
await redis.set(cacheKey, JSON.stringify(product), 'EX', 300) // TTL: 5 minutos
|
|
528
|
+
|
|
529
|
+
return product
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ✅ Invalidar cache ao atualizar
|
|
533
|
+
async function updateProduct(id: string, data: UpdateProductDto): Promise<Product> {
|
|
534
|
+
const product = await db.product.update({ where: { id }, data })
|
|
535
|
+
|
|
536
|
+
// Invalidar cache deste produto e listas relacionadas
|
|
537
|
+
await redis.del(`product:${id}`)
|
|
538
|
+
await redis.del('products:list:*') // ou usar tags
|
|
539
|
+
|
|
540
|
+
return product
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
#### Quando usar cada estratégia
|
|
545
|
+
|
|
546
|
+
| Estratégia | Quando usar |
|
|
547
|
+
|-----------|-------------|
|
|
548
|
+
| Cache-aside (lazy) | Dados lidos frequentemente, escritas ocasionais |
|
|
549
|
+
| Write-through | Dados críticos onde consistência é prioridade |
|
|
550
|
+
| Write-behind | Alta frequência de escrita, consistência eventual ok |
|
|
551
|
+
| TTL curto (< 1min) | Dados mutáveis com tolerância a leve stale |
|
|
552
|
+
| TTL longo (> 1h) | Dados estáticos ou de referência |
|
|
553
|
+
| Cache de sessão | User session, tokens temporários |
|
|
554
|
+
|
|
555
|
+
### Testes
|
|
556
|
+
|
|
557
|
+
#### Testes unitários
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
561
|
+
import { ProductService } from './product.service'
|
|
562
|
+
import { ProductRepository } from './product.repository'
|
|
563
|
+
|
|
564
|
+
describe('ProductService', () => {
|
|
565
|
+
let service: ProductService
|
|
566
|
+
let repository: ProductRepository
|
|
567
|
+
|
|
568
|
+
beforeEach(() => {
|
|
569
|
+
// ✅ Mock do repositório — não precisa de banco real
|
|
570
|
+
repository = {
|
|
571
|
+
findById: vi.fn(),
|
|
572
|
+
create: vi.fn(),
|
|
573
|
+
update: vi.fn(),
|
|
574
|
+
} as unknown as ProductRepository
|
|
575
|
+
|
|
576
|
+
service = new ProductService(repository)
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
it('lança NotFoundError quando produto não existe', async () => {
|
|
580
|
+
vi.mocked(repository.findById).mockResolvedValue(null)
|
|
581
|
+
|
|
582
|
+
await expect(service.getById('id-inexistente')).rejects.toThrow('Produto não encontrado')
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
it('retorna produto quando encontrado', async () => {
|
|
586
|
+
const product = { id: '1', name: 'Produto A', price: 100 }
|
|
587
|
+
vi.mocked(repository.findById).mockResolvedValue(product)
|
|
588
|
+
|
|
589
|
+
const result = await service.getById('1')
|
|
590
|
+
expect(result).toEqual(product)
|
|
591
|
+
})
|
|
592
|
+
})
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
#### Testes de integração (NestJS + Supertest)
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
import { Test, TestingModule } from '@nestjs/testing'
|
|
599
|
+
import { INestApplication, ValidationPipe } from '@nestjs/common'
|
|
600
|
+
import * as request from 'supertest'
|
|
601
|
+
import { ProductsModule } from '../products.module'
|
|
602
|
+
|
|
603
|
+
describe('POST /api/products', () => {
|
|
604
|
+
let app: INestApplication
|
|
605
|
+
|
|
606
|
+
beforeAll(async () => {
|
|
607
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
608
|
+
imports: [ProductsModule],
|
|
609
|
+
})
|
|
610
|
+
.overrideProvider(ProductRepository)
|
|
611
|
+
.useValue({ create: jest.fn(), findById: jest.fn() })
|
|
612
|
+
.compile()
|
|
613
|
+
|
|
614
|
+
app = module.createNestApplication()
|
|
615
|
+
app.useGlobalPipes(new ValidationPipe({ whitelist: true }))
|
|
616
|
+
await app.init()
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
afterAll(() => app.close())
|
|
620
|
+
|
|
621
|
+
it('cria produto e retorna 201', async () => {
|
|
622
|
+
const response = await request(app.getHttpServer())
|
|
623
|
+
.post('/api/products')
|
|
624
|
+
.set('Authorization', `Bearer ${testToken}`)
|
|
625
|
+
.send({ name: 'Produto Teste', price: 29.90 })
|
|
626
|
+
|
|
627
|
+
expect(response.status).toBe(201)
|
|
628
|
+
expect(response.body).toMatchObject({
|
|
629
|
+
data: { name: 'Produto Teste', price: 29.90 },
|
|
630
|
+
})
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
it('retorna 400 quando dados inválidos', async () => {
|
|
634
|
+
const response = await request(app.getHttpServer())
|
|
635
|
+
.post('/api/products')
|
|
636
|
+
.set('Authorization', `Bearer ${testToken}`)
|
|
637
|
+
.send({ name: '' }) // nome vazio, preço ausente
|
|
638
|
+
|
|
639
|
+
expect(response.status).toBe(400)
|
|
640
|
+
expect(response.body.message).toBeDefined()
|
|
641
|
+
})
|
|
642
|
+
})
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Debugging e Observabilidade
|
|
646
|
+
|
|
647
|
+
#### Logs estruturados (pino)
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
import pino from 'pino'
|
|
651
|
+
|
|
652
|
+
export const logger = pino({
|
|
653
|
+
level: process.env.LOG_LEVEL ?? 'info',
|
|
654
|
+
...(process.env.NODE_ENV !== 'production' && {
|
|
655
|
+
transport: { target: 'pino-pretty' },
|
|
656
|
+
}),
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
// ✅ Logar com contexto suficiente
|
|
660
|
+
logger.info({ userId, productId, action: 'product.updated' }, 'Produto atualizado')
|
|
661
|
+
logger.error({ error, userId, requestId }, 'Falha ao processar pagamento')
|
|
662
|
+
|
|
663
|
+
// ❌ Nunca logar dados sensíveis
|
|
664
|
+
// logger.info({ password, creditCard }) — NUNCA
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
#### Request ID para rastreabilidade
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
// ✅ Propagar request ID em todas as operações
|
|
671
|
+
app.addHook('onRequest', (req, reply, done) => {
|
|
672
|
+
req.id = req.headers['x-request-id'] as string ?? crypto.randomUUID()
|
|
673
|
+
reply.header('x-request-id', req.id)
|
|
674
|
+
done()
|
|
675
|
+
})
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
## Regras
|
|
681
|
+
|
|
682
|
+
### Nunca
|
|
683
|
+
- Expor stack traces ou detalhes de erro em respostas de produção
|
|
684
|
+
- Confiar em dados de entrada sem validação (query params, body, headers)
|
|
685
|
+
- Armazenar senhas em texto plano (usar bcrypt/argon2)
|
|
686
|
+
- Commitar secrets, API keys ou credenciais no código
|
|
687
|
+
- Fazer operações síncronas bloqueantes no event loop
|
|
688
|
+
- Processar webhooks de forma síncrona (enfileirar e responder 200 imediatamente)
|
|
689
|
+
|
|
690
|
+
### Sempre
|
|
691
|
+
- Validar e sanitizar toda entrada externa (Zod, Joi, class-validator)
|
|
692
|
+
- Usar variáveis de ambiente para configuração
|
|
693
|
+
- Incluir tratamento de erros e fallbacks em integrações externas
|
|
694
|
+
- Testar casos de erro e edge cases, não só o happy path
|
|
695
|
+
- Logar com contexto suficiente para diagnóstico
|
|
696
|
+
- Ler o código existente antes de criar novas abstrações
|
|
697
|
+
|
|
698
|
+
---
|
|
699
|
+
|
|
700
|
+
## Tratamento de Erros
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
// ✅ Erro tipado com contexto
|
|
704
|
+
export class AppError extends Error {
|
|
705
|
+
constructor(
|
|
706
|
+
public readonly message: string,
|
|
707
|
+
public readonly statusCode: number = 500,
|
|
708
|
+
public readonly code: string = 'INTERNAL_ERROR'
|
|
709
|
+
) {
|
|
710
|
+
super(message)
|
|
711
|
+
this.name = 'AppError'
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ✅ Erros de negócio explícitos
|
|
716
|
+
export class NotFoundError extends AppError {
|
|
717
|
+
constructor(resource: string, id: string) {
|
|
718
|
+
super(`${resource} com id '${id}' não encontrado`, 404, 'NOT_FOUND')
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export class UnauthorizedError extends AppError {
|
|
723
|
+
constructor(message = 'Não autorizado') {
|
|
724
|
+
super(message, 401, 'UNAUTHORIZED')
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
## Checklist de Conclusão
|
|
732
|
+
|
|
733
|
+
- [ ] Entrada validada (Zod / Joi / class-validator)
|
|
734
|
+
- [ ] Autenticação e autorização verificadas
|
|
735
|
+
- [ ] Erros tipados e tratados (não vazar detalhes em produção)
|
|
736
|
+
- [ ] Logs estruturados com contexto adequado
|
|
737
|
+
- [ ] Testes unitários e/ou de integração
|
|
738
|
+
- [ ] Rate limiting considerado (se endpoint público)
|
|
739
|
+
- [ ] Cache implementado onde faz sentido
|
|
740
|
+
- [ ] Operações destrutivas com confirmação/idempotência
|
|
741
|
+
|
|
742
|
+
---
|
|
743
|
+
|
|
744
|
+
## Output
|
|
745
|
+
|
|
746
|
+
| Artefato | Descrição |
|
|
747
|
+
|----------|-----------|
|
|
748
|
+
| Endpoint(s) | Rota com validação, autenticação e tratamento de erro |
|
|
749
|
+
| Service/Use case | Lógica de negócio isolada e testável |
|
|
750
|
+
| Worker/Job | Processamento assíncrono com retry e observabilidade |
|
|
751
|
+
| Testes | Unitários e/ou integração cobrindo happy path e erros |
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
755
|
+
## Mensagem de Conclusão
|
|
756
|
+
|
|
757
|
+
```
|
|
758
|
+
Implementação backend concluída!
|
|
759
|
+
|
|
760
|
+
Framework: NestJS
|
|
761
|
+
ORM: {Prisma / TypeORM / Drizzle}
|
|
762
|
+
Funcionalidade: {descrição do que foi implementado}
|
|
763
|
+
|
|
764
|
+
Segurança: {validação de entrada / autenticação / autorização}
|
|
765
|
+
Testes: {criados / pendentes}
|
|
766
|
+
Cache: {implementado / não necessário}
|
|
767
|
+
|
|
768
|
+
Próximo passo: {rodar testes / fazer deploy / integrar com frontend}
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
## Recursos Adicionais
|
|
774
|
+
|
|
775
|
+
- **RabbitMQ**: Ver skill `eng-rabbitmq` para operações avançadas de mensageria
|
|
776
|
+
- **Referências**: Veja [references/](references/) para links de documentação local
|