---
name: gen-endpoint
description: "Add a REST API endpoint with Swagger docs, auth decorators, DTOs, and service method. Examples: \"Add POST /products endpoint\", \"Create search endpoint\", \"Add bulk delete endpoint\""
---

# Generate REST API Endpoint

Adds a new endpoint following this project's controller pattern.

## Arguments

```
/gen-endpoint <METHOD> <path> [--module <ModuleName>] [--public] [--paginated] [--cursor]
```

- `METHOD`: GET, POST, PATCH, PUT, DELETE
- `path`: Route path (e.g., `/products/search`)
- `--module`: Target module (required if ambiguous)
- `--public`: Use `@ApiPublic` (no auth) instead of `@ApiAuth`
- `--paginated`: Add offset pagination
- `--cursor`: Add cursor pagination

## Pattern — MUST MATCH EXACTLY

### Controller Method

```typescript
// Authenticated endpoint
@ApiAuth({
  summary: 'Create a product',
  type: ProductDto,
  statusCode: HttpStatus.CREATED,
  errorResponses: [400, 401, 403, 422, 500],
})
@Post()
async createProduct(
  @Body() dto: CreateProductDto,
  @CurrentUserSession('user') user: CurrentUserSession['user'],
): Promise<ProductDto> {
  return await this.productService.create(user.id, dto);
}

// Public endpoint
@ApiPublic({
  summary: 'Search products',
  type: OffsetPaginatedProductDto,
  isPaginated: true,
})
@Get('search')
async searchProducts(
  @Query() dto: SearchProductsDto,
): Promise<OffsetPaginatedDto<ProductDto>> {
  return await this.productService.search(dto);
}

// Parameterized endpoint
@ApiAuth({ summary: 'Get product by ID', type: ProductDto })
@ApiParam({ name: 'id', type: 'string' })
@Get(':id')
async getProduct(
  @Param('id', ParseUUIDPipe) id: Uuid,
): Promise<ProductDto> {
  return await this.productService.findOne(id);
}
```

### Decorator Usage

| Scenario | Decorator |
|----------|-----------|
| Authenticated | `@ApiAuth({ summary, type })` |
| Public | `@ApiPublic({ summary, type })` |
| Paginated (offset) | `@ApiAuth({ ..., isPaginated: true })` |
| Paginated (cursor) | `@ApiAuth({ ..., isPaginated: true, paginationType: 'cursor' })` |
| UUID param | `@Param('id', ParseUUIDPipe) id: Uuid` + `@ApiParam({ name: 'id', type: 'string' })` |
| Current user | `@CurrentUserSession('user') user: CurrentUserSession['user']` |
| Full session | `@CurrentUserSession() session: CurrentUserSession` |

### Import Paths

```typescript
import { AuthGuard } from '@/auth/auth.guard';
import { Uuid } from '@/common/types/common.type';
import { CurrentUserSession } from '@/decorators/auth/current-user-session.decorator';
import { ApiAuth, ApiPublic } from '@/decorators/http.decorators';
```

### Service Method

```typescript
// Always throw translated errors
async findOne(id: Uuid): Promise<ProductDto> {
  const entity = await this.repository.findOne({ where: { id } });
  if (!entity) {
    throw new NotFoundException(this.i18nService.t('product.notFound'));
  }
  return entity;
}

// Create — return the created entity
async create(userId: string, dto: CreateProductDto): Promise<ProductDto> {
  const entity = this.repository.create({ ...dto, userId });
  return await this.repository.save(entity);
}

// Update — use .update() then re-fetch
async update(id: Uuid, dto: UpdateProductDto): Promise<ProductDto> {
  await this.repository.update(id, dto);
  return await this.findOne(id);
}

// Delete — soft delete
async delete(id: Uuid): Promise<HttpStatus> {
  await this.repository.findOneByOrFail({ id });
  await this.repository.softDelete(id);
  return HttpStatus.OK;
}
```

## Checklist

```
- [ ] Controller method added with proper decorators
- [ ] Request DTO created (if POST/PATCH/PUT)
- [ ] Response DTO created or reused
- [ ] Service method implemented
- [ ] i18n keys added for error messages
- [ ] Swagger docs accurate (test via /docs)
- [ ] `pnpm lint` passes
```
