Appearance
Code Style Guide
Padrões de código obrigatórios para toda a base de código.
1. Princípios Invioláveis
IMPORTANTE: Estas regras são absolutas e não devem ser violadas em nenhuma circunstância.
1.1 Nunca Comentários no Código
O código deve ser autoexplicativo. Não adicionar comentários em:
- Prisma Schema
- Arquivos TypeScript
- DTOs, Mappers, Services, Repositories, Controllers
Se o código precisa de comentário, refatore para ser mais claro.
1.2 Nunca Usar Tipo any
O tipo any é proibido em toda a base de código. Alternativas:
| Situação | Usar em vez de any |
|---|---|
| Prisma where clauses | Prisma.EntityWhereInput |
| Prisma data objects | Prisma.EntityUpdateInput |
| Objetos JSON/variáveis | Record<string, unknown> ou interface específica |
| Arrays de entidades | EntityType[] com interface definida |
| Parâmetros genéricos | Generics <T> |
| Unknown values | unknown (requer type guard) |
typescript
// ❌ PROIBIDO
const where: any = { name: 'foo' };
function handle(data: any) {}
// ✅ CORRETO
const where: Prisma.ContractWhereInput = { name: 'foo' };
function handle(data: CreateContractDto) {}1.3 Importações Absolutas
Sempre usar path aliases configurados no tsconfig.json:
typescript
// ❌ PROIBIDO
import { PrismaService } from '../../../services/prisma.service';
import { SomeDto } from '../../data/dto/some.dto';
// ✅ CORRETO
import { PrismaService } from '@services/prisma.service';
import { SomeDto } from '@modules/contracts/data/dto/some.dto';Path aliases disponíveis:
@/*→src/*@common/*→src/common/*@config/*→src/config/*@modules/*→src/modules/*@services/*→src/services/*
1.4 Tipos, Interfaces e Constantes Fora de Service/Repository/Mapper/Controller
Toda interface, type, const/enum top-level e includes Prisma reutilizáveis devem ficar em domain/ do módulo correspondente, nunca declarados em arquivos de service, repository, mapper ou controller.
Estrutura recomendada por módulo:
modules/<mod>/domain/
├── interfaces/ # ProviderFormFieldInput, OnboardingContext, ActorContext...
├── constants/ # WORKFLOW_STEPS, EMAIL_TEMPLATES...
├── enums/ # ContractHistoryAction, OnboardingStatus...
├── utils/ # buildOnboardingToken, resolveFieldType...
└── entities/typescript
// ❌ PROIBIDO declarado dentro do service/repository
@Injectable()
export class OnboardingRepository {
// não declare interface/type aqui
}
interface ProviderFormFieldInput { ... }
const STATUS_MAP = { ... };
// ✅ CORRETO em domain/interfaces/provider-form-field.interface.ts
export interface ProviderFormFieldInput { ... }
// service só importa
import { ProviderFormFieldInput } from '@modules/onboarding/domain/interfaces/provider-form-field.interface';Exceção: variáveis locais a uma função (ex: const filtered = array.filter(...)) e tipos descartáveis usados apenas como argumento de uma função privada do mesmo arquivo. Em dúvida, sempre mova para domain/.
1.5 PrismaService: pool único via módulo global
Existe um único PrismaService na aplicação, provido por um @Global() PrismaModule (em src/services/prisma.module.ts), importado uma só vez no AppModule. Toda a aplicação compartilha esse mesmo client, e, portanto, um único pool de conexões.
Cada instância de PrismaClient/PrismaService abre seu próprio pool (num_cpus * 2 + 1 conexões por padrão). Declarar PrismaService no providers de cada módulo cria uma instância por módulo → dezenas de pools → estouro do max_connections do Postgres em produção. Esse bug já derrubou o banco em prod.
- PROIBIDO colocar
PrismaServicenoprovidersde qualquer módulo que não seja oPrismaModule. - PROIBIDO
new PrismaClient()em qualquer lugar do código. - PROIBIDO abrir conexões diretas ao banco (
pg,Pool, etc.); use sempre oPrismaServiceglobal. - Para usar: apenas injete
PrismaServiceno construtor (constructor(private readonly prisma: PrismaService) {}). O módulo global já o disponibiliza em qualquer provider, semimportsnemproviderslocais. - Mesma regra vale para a ai-api (
src/common/prisma/prisma.module.ts), que já segue esse padrão.
ts
// ❌ ERRADO cada módulo recria o client = novo pool de conexões
@Module({
providers: [FooService, FooRepository, PrismaService],
})
export class FooModule {}
// ✅ CORRETO módulo não declara PrismaService; injeta o global
@Module({
providers: [FooService, FooRepository],
})
export class FooModule {}2. Nomenclatura
| Tipo | Convenção | Exemplo |
|---|---|---|
| Classes | PascalCase | ContractService |
| Métodos | camelCase | findAllByCompany() |
| Variáveis | camelCase | contractList |
| Constantes | UPPER_SNAKE | MAX_PAGE_SIZE |
| Arquivos | kebab-case | contract.service.ts |
| DTOs | PascalCase + Dto | CreateContractDto |
| Entities | PascalCase + Entity | ContractEntity |
3. Princípios Clean Code
- Single Responsibility: Uma classe/método = uma responsabilidade
- Dependency Injection: Sempre via constructor
- No Magic Numbers: Usar constantes nomeadas
- Early Return: Evitar if/else aninhados
- Self-Documenting Code: Nomes descritivos
- Immutability: Preferir readonly e objetos imutáveis
- Error Handling: Exceptions tipadas e tratadas
4. Padrões por Camada
4.1 Controllers
typescript
@ApiTags('contracts')
@ApiBearerAuth()
@ApiHeader({
name: 'x-company-id',
description: 'ID da empresa para contexto da requisição',
required: true,
})
@Controller('contracts')
export class ContractController {
constructor(private readonly contractService: ContractService) {}
@Get()
@ApiOperation({ summary: 'Listar contratos' })
@ApiResponse({ status: 200, type: [ContractResponseDto] })
async findAll(
@CurrentCompany() company: CurrentCompanyData,
@Query() filters: SearchContractDto,
) {
return this.contractService.findAll(company.id, filters);
}
}4.2 Services
typescript
@Injectable()
export class ContractService {
constructor(private readonly contractRepository: ContractRepository) {}
async findAll(companyId: string, filters: SearchContractDto) {
const { data, total, page, pageSize } =
await this.contractRepository.findAll(companyId, filters);
return {
data: ContractMapper.toListResponseDto(data),
meta: buildPaginationMeta(total, page, pageSize),
};
}
}4.3 Repositories
typescript
@Injectable()
export class ContractRepository {
constructor(private readonly prisma: PrismaService) {}
async findAll(companyId: string, filters: SearchContractDto) {
const { skip, take, page, pageSize } = getPaginationParams(filters);
const where: Prisma.ContractWhereInput = {
companyId,
deletedAt: null,
};
const [data, total] = await Promise.all([
this.prisma.contract.findMany({ where, skip, take }),
this.prisma.contract.count({ where }),
]);
return { data, total, page, pageSize };
}
}4.4 DTOs
OBRIGATÓRIO: Todo campo @IsString() deve ter @MaxLength(TEXT_LENGTH.*) ou usar o decorator composto @TextField({ tier }). Sem exceção. Detalhes e tiers canônicos em input-length-limits.md.
DTOs de listagem devem herdar PaginationQuery (@common/types/pagination.type) para receber search já blindado com @MaxLength(TEXT_LENGTH.SEARCH).
typescript
import { TextField } from '@common/decorators';
import { TEXT_LENGTH } from '@common/constants/text-length.constants';
import { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
export class CreateContractDto {
@TextField({ tier: 'SHORT', description: 'Nome do contrato' })
@IsNotEmpty()
name: string;
@TextField({ tier: 'XLONG', optional: true })
description?: string;
@IsOptional()
@IsString()
@MaxLength(TEXT_LENGTH.MEDIUM)
externalReference?: string;
}Forma direta (sem @TextField) é válida quando o campo precisa de decorators adicionais que não compõem bem (ex: @IsEmail, @Matches). Sempre importe TEXT_LENGTH de @common/constants/text-length.constants.
4.5 Mappers
typescript
export class ContractMapper {
static toResponseDto(contract: Contract): ContractResponseDto {
return {
id: contract.id,
name: contract.name,
status: contract.status.toLowerCase(),
progress: contract.progress,
createdAt: contract.createdAt,
};
}
static toListResponseDto(contracts: Contract[]): ContractResponseDto[] {
return contracts.map((c) => this.toResponseDto(c));
}
}5. Armazenamento de Arquivos
Todos os arquivos devem ser armazenados na tabela files. As associações são feitas por tabelas de relacionamento:
| Entidade | Tabela de relacionamento |
|---|---|
| Company | company_files |
| User | users_files |
Regras:
- Nunca duplicar dados de arquivo (nome, path, tamanho, mime) em outras tabelas
- Sempre criar o registro em
filese associar via tabela de relacionamento - Tabelas de contexto (ex:
onboarding_documents) referenciamfilesviafileId(FK) - Nunca criar tabelas que armazenam
fileName,fileUrl,fileSizediretamente
6. Soft Delete
Todas as entidades principais têm:
typescript
{
createdAt: DateTime
updatedAt: DateTime
deletedAt: DateTime? // null = ativo
}Sempre filtrar deletedAt nas queries:
typescript
where: { deletedAt: null }
async softDelete(id: string) {
return this.prisma.entity.update({
where: { id },
data: { deletedAt: new Date() }
});
}Regras de Código
| Regra | Descrição |
|---|---|
| Sem comentários | Não adicionar comentários no código. O código deve ser autoexplicativo |
| Script Setup | Usar sempre <script setup lang="ts"> |
| TypeScript | Todo código deve ser tipado |
| Sem tipar retorno | NÃO tipar retorno de funções. O TypeScript infere automaticamente |
| Sem if inline | PROIBIDO if em uma linha. Sempre usar bloco { } com quebra de linha |
| Sem return inline | PROIBIDO return na mesma linha do if. Sempre dentro do bloco { } |
| Linha vazia após if | Sempre deixar uma linha vazia após o fechamento } de um bloco if |
| Linha vazia após const/let | Sempre deixar uma linha vazia após cada declaração const ou let |
| Sem aninhamento | PROIBIDO aninhar if ou for. Usar early returns e métodos auxiliares |
| Responsabilidade única | Cada método deve ter apenas uma responsabilidade. Extrair lógica em funções menores |
Documento atualizado em Janeiro 2026