Skip to content

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çãoUsar em vez de any
Prisma where clausesPrisma.EntityWhereInput
Prisma data objectsPrisma.EntityUpdateInput
Objetos JSON/variáveisRecord<string, unknown> ou interface específica
Arrays de entidadesEntityType[] com interface definida
Parâmetros genéricosGenerics <T>
Unknown valuesunknown (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 PrismaService no providers de qualquer módulo que não seja o PrismaModule.
  • PROIBIDO new PrismaClient() em qualquer lugar do código.
  • PROIBIDO abrir conexões diretas ao banco (pg, Pool, etc.); use sempre o PrismaService global.
  • Para usar: apenas injete PrismaService no construtor (constructor(private readonly prisma: PrismaService) {}). O módulo global já o disponibiliza em qualquer provider, sem imports nem providers locais.
  • 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

TipoConvençãoExemplo
ClassesPascalCaseContractService
MétodoscamelCasefindAllByCompany()
VariáveiscamelCasecontractList
ConstantesUPPER_SNAKEMAX_PAGE_SIZE
Arquivoskebab-casecontract.service.ts
DTOsPascalCase + DtoCreateContractDto
EntitiesPascalCase + EntityContractEntity

3. Princípios Clean Code

  1. Single Responsibility: Uma classe/método = uma responsabilidade
  2. Dependency Injection: Sempre via constructor
  3. No Magic Numbers: Usar constantes nomeadas
  4. Early Return: Evitar if/else aninhados
  5. Self-Documenting Code: Nomes descritivos
  6. Immutability: Preferir readonly e objetos imutáveis
  7. 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:

EntidadeTabela de relacionamento
Companycompany_files
Userusers_files

Regras:

  • Nunca duplicar dados de arquivo (nome, path, tamanho, mime) em outras tabelas
  • Sempre criar o registro em files e associar via tabela de relacionamento
  • Tabelas de contexto (ex: onboarding_documents) referenciam files via fileId (FK)
  • Nunca criar tabelas que armazenam fileName, fileUrl, fileSize diretamente

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

RegraDescrição
Sem comentáriosNão adicionar comentários no código. O código deve ser autoexplicativo
Script SetupUsar sempre <script setup lang="ts">
TypeScriptTodo código deve ser tipado
Sem tipar retornoNÃO tipar retorno de funções. O TypeScript infere automaticamente
Sem if inlinePROIBIDO if em uma linha. Sempre usar bloco { } com quebra de linha
Sem return inlinePROIBIDO return na mesma linha do if. Sempre dentro do bloco { }
Linha vazia após ifSempre deixar uma linha vazia após o fechamento } de um bloco if
Linha vazia após const/letSempre deixar uma linha vazia após cada declaração const ou let
Sem aninhamentoPROIBIDO aninhar if ou for. Usar early returns e métodos auxiliares
Responsabilidade únicaCada método deve ter apenas uma responsabilidade. Extrair lógica em funções menores


Documento atualizado em Janeiro 2026