Skip to main content

backend-nestjs

Use ao implementar backend em NestJS/Node.js quando project.config.md indicar esta stack. Cobre estrutura hexagonal com módulos NestJS, DTOs com class-validator, guards JWT/ABAC, interceptors, OpenAPI via @nestjs/swagger, Prometheus, endpoint /version e padrão de fixtures com afterEach. Baseado em nestjs-patterns (ECC/affaan-m) e adaptado aos padrões deste projeto.

ソース情報

リポジトリ
marquesmarcelo/dev-team
ソースの最終更新活動
2026年10月13日 12:41
検出された SKILL.md の言語
ポルトガル語
スター
2
フォーク
0

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
backend-nestjs
description
Use ao implementar backend em NestJS/Node.js quando project.config.md indicar esta stack. Cobre estrutura hexagonal com módulos NestJS, DTOs com class-validator, guards JWT/ABAC, interceptors, OpenAPI via @nestjs/swagger, Prometheus, endpoint /version e padrão de fixtures com afterEach. Baseado em nestjs-patterns (ECC/affaan-m) e adaptado aos padrões deste projeto.
# Backend NestJS + Node.js > Padrões universais do projeto (`CLAUDE.md`) se aplicam: UUID, deleção lógica, > hexagonal, CQRS-leve, Value Objects, etc. Esta skill cobre o que é > específico do NestJS. ## Estrutura de pastas — hexagonal adaptada ao NestJS > **SOLID e Clean Code são não-negociáveis.** DIP já está expresso nas portas e adaptadores (hexagonal). Mantenha SRP: cada classe tem uma responsabilidade. Nomes de métodos devem ser autoexplicativos — prefira verbos longos a abreviações. ``` src/ domain/ entity/ ← entidades e Value Objects (classes com validação) port/ ← interfaces (repository, services externos) usecase/ command/<entidade>/ ← CriarProcessoUseCase, etc. query/<entidade>/ ← ListarProcessosUseCase, etc. adapter/ http/ ← controllers NestJS (finos — só parse + delegação) dto/ ← DTOs de request/response com class-validator <entidade>.controller.ts <entidade>.module.ts postgres/ ← implementação de repositórios com Kysely (sem ORM) <entidade>.repository.ts external/ ← clientes de serviços externos common/ ← transversais (filtros, guards globais, interceptors) filters/ guards/ interceptors/ pipes/ config/ ← configuração de ambiente com validação Zod/Joi app.module.ts main.ts ``` **Regra:** Controllers orquestram parse HTTP → use case → response DTO. Lógica de negócio fica nos use cases, nunca nos controllers. --- ## Bootstrap e configuração global ```typescript // main.ts async function bootstrap() { const app = await NestFactory.create(AppModule, { bufferLogs: true }) // Validação global de DTOs — obrigatória app.useGlobalPipes(new ValidationPipe({ whitelist: true, // remove campos não declarados no DTO forbidNonWhitelisted: true, // erro se vier campo extra (não só ignorar) transform: true, transformOptions: { enableImplicitConversion: true }, })) // Serialização global — oculta campos marcados com @Exclude() app.useGlobalInterceptors( new ClassSerializerInterceptor(app.get(Reflector)) ) // Filtro global de exceções (shape consistente) app.useGlobalFilters(new HttpExceptionFilter()) // OpenAPI — gerado automaticamente, nunca escrito à mão const config = new DocumentBuilder() .setTitle(process.env.APP_NAME ?? 'API') .setVersion(process.env.APP_VERSION ?? '0.0.1') .addBearerAuth() .build() SwaggerModule.setup('api/docs', app, SwaggerModule.createDocument(app, config)) await app.listen(process.env.PORT ?? 3001) } bootstrap() ``` --- ## Módulos, Controllers e Providers ```typescript // adapter/http/processo.module.ts @Module({ controllers: [ProcessoController], providers: [ ProcessoRepository, // implementação do port CriarProcessoUseCase, ListarProcessosUseCase, BuscarProcessoPorIdUseCase, ExcluirProcessoUseCase, ], }) export class ProcessoModule {} // adapter/http/processo.controller.ts @ApiTags('processos') @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Controller('api/v1/processos') export class ProcessoController { constructor( private readonly criar: CriarProcessoUseCase, private readonly listar: ListarProcessosUseCase, private readonly buscar: BuscarProcessoPorIdUseCase, private readonly excluir: ExcluirProcessoUseCase, ) {} @Post() @ApiCreatedResponse({ type: ProcessoResponseDto }) async create(@Body() dto: CriarProcessoDto, @Req() req: AuthRequest) { return this.criar.executar({ ...dto, autorId: req.user.id }) } @Get() @ApiOkResponse({ type: ListaProcessosResponseDto }) async findAll(@Query() query: ListarProcessosQuery) { return this.listar.executar(query) } @Get(':id') async findOne(@Param('id', ParseUUIDPipe) id: string) { return this.buscar.executar(id) } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) async remove(@Param('id', ParseUUIDPipe) id: string, @Req() req: AuthRequest) { await this.excluir.executar({ id, autorId: req.user.id }) } } ``` --- ## Persistência: pg + Kysely (sem ORM) ```bash npm install pg kysely npm install -D @types/pg ``` ```typescript // infrastructure/database/kysely.ts import { Kysely, PostgresDialect } from 'kysely'; import { Pool } from 'pg'; export const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL }), }), }); ``` ```typescript // adapter/processo.repository.ts @Injectable() export class ProcessoRepository implements IProcessoRepository { constructor(private readonly db: Kysely<Database>) {} async findById(id: string): Promise<Processo | null> { const row = await this.db .selectFrom('processos') .selectAll() .where('id', '=', id) .executeTakeFirst(); return row ? ProcessoMapper.toDomain(row) : null; } async save(processo: Processo): Promise<void> { await this.db .insertInto('processos') .values(ProcessoMapper.toPersistence(processo)) .onConflict(oc => oc.column('id').doUpdateSet(ProcessoMapper.toPersistence(processo))) .execute(); } } ``` Kysely é type-safe, sem mágica, sem migrations automáticas — migrations ficam em arquivos SQL versionados. Nada de `@Entity`, `@InjectRepository`, `TypeOrmModule.forFeature` ou decorators de coluna. --- ## DTOs com class-validator ```typescript // adapter/http/dto/criar-processo.dto.ts import { IsString, IsNotEmpty, IsOptional, IsUUID, Length } from 'class-validator' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' export class CriarProcessoDto { @ApiProperty() @IsString() @IsNotEmpty() @Length(3, 255) descricao: string @ApiPropertyOptional() @IsOptional() @IsUUID() responsavelId?: string } // Response DTO — nunca retornar entidade ORM direto export class ProcessoResponseDto { @ApiProperty() id: string @ApiProperty() descricao: string @ApiProperty() status: string @ApiProperty() criadoEm: Date @Exclude() // ocultar campos internos — funciona com ClassSerializerInterceptor excluídoEm?: Date static fromDomain(processo: Processo): ProcessoResponseDto { return plainToInstance(ProcessoResponseDto, processo) } } ``` --- ## Exception Filter — shape consistente ```typescript // common/filters/http-exception.filter.ts @Catch() export class HttpExceptionFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp() const res = ctx.getResponse<Response>() const req = ctx.getRequest<Request>() if (exception instanceof HttpException) { const status = exception.getStatus() const body = exception.getResponse() return res.status(status).json({ path: req.url, timestamp: new Date().toISOString(), error: typeof body === 'string' ? { message: body } : body, }) } // Erro inesperado — logar e não vazar detalhes console.error('Erro não tratado:', exception) return res.status(500).json({ path: req.url, timestamp: new Date().toISOString(), error: { message: 'Erro interno do servidor' }, }) } } ``` --- ## Configuração de ambiente com validação ```typescript // config/configuration.ts import { z } from 'zod' const EnvSchema = z.object({ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), PORT: z.coerce.number().default(3001), DATABASE_URL: z.string().url(), JWT_SECRET: z.string().min(32), APP_NAME: z.string().default('API'), APP_VERSION: z.string().default('0.0.0'), BUILD_DATE: z.string().optional(), GIT_COMMIT: z.string().optional(), RUN_TESTS: z.enum(['true', 'false']).default('false'), }) export type Env = z.infer<typeof EnvSchema> export function validateEnv(config: Record<string, unknown>): Env { const result = EnvSchema.safeParse(config) if (!result.success) { throw new Error(`Variáveis de ambiente inválidas:\n${result.error.message}`) } return result.data } ``` ```typescript // config/config.module.ts (global) @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, validate: validateEnv, }), ], }) export class AppConfigModule {} ``` --- ## Observabilidade: /healthz, /readyz, /metrics, /version ```typescript // adapter/http/health.controller.ts @Controller() export class HealthController { constructor( private health: HealthCheckService, private db: Kysely<Database>, private config: ConfigService<Env>, ) {} @Get('healthz') @HealthCheck() liveness() { return { status: 'ok' } } @Get('readyz') @HealthCheck() readiness() { return this.health.check([ async () => { await sql`SELECT 1`.execute(this.db) return { database: { status: 'up' } } }, ]) } @Get('version') version() { return { app: this.config.get('APP_NAME'), backend: this.config.get('APP_VERSION'), buildDate: this.config.get('BUILD_DATE') ?? null, commit: this.config.get('GIT_COMMIT') ?? null, } } } ``` **Prometheus:** usar `@willsoto/nestjs-prometheus` — registra métricas HTTP automaticamente via interceptor, sem instrumentar cada endpoint: ```bash npm install @willsoto/nestjs-prometheus prom-client ``` ```typescript // app.module.ts PrometheusModule.register({ defaultMetrics: { enabled: true }, path: '/metrics', }) ``` --- ## Auth JWT + port ABAC ```typescript // domain/port/authorization-checker.port.ts export interface AuthorizationChecker { podeExecutar(usuarioId: string, acao: string, recursoId?: string): Promise<boolean> } // Implementação NoOp (até dev-auth implementar ABAC real) @Injectable() export class NoOpAuthorizationChecker implements AuthorizationChecker { async podeExecutar(): Promise<boolean> { return true } } // Guard JWT padrão @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { handleRequest<T>(err: Error, user: T): T { if (err || !user) throw err ?? new UnauthorizedException() return user } } ``` --- ## Testes **Unitário (use case isolado):** ```typescript describe('CriarProcessoUseCase', () => { let useCase: CriarProcessoUseCase let repo: jest.Mocked<ProcessoRepository> beforeEach(() => { repo = { criar: jest.fn(), buscarPorId: jest.fn() } as any useCase = new CriarProcessoUseCase(repo) }) it('deve criar processo', async () => { repo.criar.mockResolvedValue({ id: 'uuid', descricao: 'Teste' } as Processo) const result = await useCase.executar({ descricao: 'Teste', autorId: 'user-id' }) expect(result.descricao).toBe('Teste') }) }) ``` **Integração (NestApplication real):** ```typescript describe('ProcessoController (e2e)', () => { let app: INestApplication beforeAll(async () => { const module = await Test.createTestingModule({ imports: [AppModule], }).compile() app = module.createNestApplication() app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })) await app.init() }) afterAll(() => app.close()) it('POST /api/v1/processos → 201', async () => { return request(app.getHttpServer()) .post('/api/v1/processos') .set('Authorization', `Bearer ${token}`) .send({ descricao: 'Processo de teste' }) .expect(201) }) }) ``` **RUN_TESTS:** ver regra em `CLAUDE.md` — usar `DATABASE_URL_TEST` para banco isolado.
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る