# TMC Test Task — Claude Code Prompt > Запусти этот промт из корня проекта (например `~/projects/dev-landing`) --- ## Цель Создать fullstack-приложение: **backend API на NestJS** + **лендинг на Nuxt 3**. Монорепозиторий с двумя папками: `backend/` и `frontend/`. Работай итеративно: сначала scaffold → потом реализация по фичам. Не переходи к следующему шагу, пока текущий не работает. --- ## Структура репозитория ``` internetlab-test-task/ ├── backend/ │ ├── src/ │ │ ├── main.ts │ │ ├── app.module.ts │ │ ├── contact/ │ │ │ ├── contact.module.ts │ │ │ ├── contact.controller.ts │ │ │ ├── contact.service.ts │ │ │ ├── dto/ │ │ │ │ └── create-contact.dto.ts │ │ │ └── interfaces/ │ │ │ └── contact.interface.ts │ │ ├── ai/ │ │ │ ├── ai.module.ts │ │ │ └── ai.service.ts │ │ ├── mail/ │ │ │ ├── mail.module.ts │ │ │ └── mail.service.ts │ │ ├── health/ │ │ │ └── health.controller.ts │ │ ├── metrics/ │ │ │ ├── metrics.module.ts │ │ │ ├── metrics.controller.ts │ │ │ └── metrics.service.ts │ │ ├── logger/ │ │ │ └── logger.service.ts │ │ └── storage/ │ │ └── file-storage.service.ts │ ├── data/ │ │ ├── logs/ ← логи запросов (создаётся автоматически) │ │ ├── metrics.json ← статистика обращений │ │ └── rate-limit/ ← данные rate limiting │ ├── .env.example │ ├── tsconfig.json │ └── package.json └── frontend/ ├── app/ │ ├── pages/ │ │ └── index.vue │ ├── components/ │ │ ├── HeroSection.vue │ │ ├── AboutSection.vue │ │ ├── SkillsSection.vue │ │ ├── ProjectsSection.vue │ │ └── ContactForm.vue │ ├── composables/ │ │ └── useContact.ts │ └── assets/ │ └── styles/ │ └── main.scss ├── .env.example ├── nuxt.config.ts └── package.json ``` --- ## BACKEND ### Технологии - **Runtime**: Node.js + TypeScript - **Framework**: NestJS 10 + Fastify adapter (`@nestjs/platform-fastify`) - **Package manager**: pnpm - **Validation**: `class-validator` + `class-transformer` - **Docs**: `@nestjs/swagger` - **Rate limiting**: `@nestjs/throttler` - **Mail**: `nodemailer` - **Logging**: `winston` - **AI**: `openai` (OpenAI-compatible SDK, используем Groq baseURL) - **Config**: `@nestjs/config` + `dotenv` ### package.json (backend) ```json { "name": "dev-landing-backend", "scripts": { "dev": "nest start --watch", "build": "nest build", "start": "node dist/main.js", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"" } } ``` Зависимости: `@nestjs/common`, `@nestjs/core`, `@nestjs/platform-fastify`, `@nestjs/config`, `@nestjs/swagger`, `@nestjs/throttler`, `class-validator`, `class-transformer`, `reflect-metadata`, `rxjs`, `nodemailer`, `winston`, `openai`, `uuid` Dev: `@nestjs/cli`, `@nestjs/schematics`, `@types/node`, `@types/nodemailer`, `@types/uuid`, `typescript`, `ts-node`, `eslint`, `@antfu/eslint-config` ### Переменные окружения (`.env.example`) ```env # Server PORT=3001 NODE_ENV=development # CORS FRONTEND_URL=http://localhost:3000 # Mail (SMTP) SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_USER=your@gmail.com SMTP_PASS=your-app-password MAIL_FROM=your@gmail.com MAIL_TO=owner@gmail.com # AI (Groq) GROQ_API_KEY=your-groq-api-key GROQ_MODEL=llama-3.3-70b-versatile # Rate limiting THROTTLE_TTL=60000 THROTTLE_LIMIT=5 ``` ### Архитектура и реализация #### `main.ts` - NestJS + Fastify adapter - Включить `ValidationPipe` глобально (с `transform: true`, `whitelist: true`) - Настроить CORS: разрешить `FRONTEND_URL` из env - Настроить Swagger: `DocumentBuilder` → title, description, version → `SwaggerModule.setup('api/docs', app, document)` - Порт из `process.env.PORT ?? 3001` #### `app.module.ts` Импортировать: `ConfigModule.forRoot({ isGlobal: true })`, `ThrottlerModule.forRootAsync(...)`, `ContactModule`, `HealthModule`, `MetricsModule` #### DTO (`create-contact.dto.ts`) ```typescript export class CreateContactDto { @IsString() @IsNotEmpty() @MaxLength(100) @ApiProperty({ example: "Иван Иванов" }) name: string; @IsString() @IsNotEmpty() @Matches(/^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/) @ApiProperty({ example: "+7 999 123-45-67" }) phone: string; @IsEmail() @ApiProperty({ example: "ivan@example.com" }) email: string; @IsString() @IsOptional() @MaxLength(1000) @ApiProperty({ example: "Хочу обсудить проект", required: false }) comment?: string; } ``` #### `ContactController` (`POST /api/contact`) - Декоратор `@UseGuards(ThrottlerGuard)` - Принимает `CreateContactDto` - Вызывает `contactService.handleContact(dto)` - Возвращает `{ success: true, message: string, ai: { sentiment, category, autoReply } }` - Обработка ошибок: если mail упал — вернуть 500 с понятным сообщением; если AI упал — продолжить без AI (graceful fallback) - Все ошибки логировать через `LoggerService` #### `ContactService` Метод `handleContact(dto: CreateContactDto)`: 1. Логировать входящий запрос через `LoggerService` (в файл) 2. Вызвать `aiService.analyzeContact(dto)` — получить `{ sentiment, category, autoReply }`. Если упал — `null`, продолжить 3. Вызвать `mailService.sendOwnerNotification(dto, aiResult)` 4. Вызвать `mailService.sendUserConfirmation(dto, aiResult)` 5. Обновить метрики через `metricsService.increment(category)` 6. Вернуть результат #### `AiService` Используй `openai` SDK с Groq baseURL: ```typescript import OpenAI from "openai"; const groq = new OpenAI({ apiKey: process.env.GROQ_API_KEY, baseURL: "https://api.groq.com/openai/v1", }); ``` Метод `analyzeContact(dto)`: - Промпт (системный): "Ты — аналитик обращений. Отвечай ТОЛЬКО валидным JSON без markdown и пояснений." - Промпт (пользователь): передать имя, email, комментарий - Ожидаемый JSON-ответ от модели: ```json { "sentiment": "positive" | "neutral" | "negative", "category": "question" | "partnership" | "job" | "spam" | "other", "autoReply": "текст автоответа на русском языке (2-3 предложения)" } ``` - Graceful fallback: обернуть в `try/catch`, при ошибке логировать и вернуть `null` - Таймаут запроса: 10 секунд #### `MailService` `sendOwnerNotification(dto, aiResult)`: - Тема: `Новое обращение от ${dto.name}` - HTML-письмо: имя, телефон, email, комментарий, блок AI-анализа (если есть) `sendUserConfirmation(dto, aiResult)`: - Тема: `Спасибо за обращение, ${dto.name}!` - HTML-письмо: подтверждение получения + `autoReply` от AI (если есть) или дефолтный текст #### `LoggerService` Использовать `winston`: - Транспорты: Console + File (`data/logs/requests-YYYY-MM-DD.log`) - Формат файла: JSON с `timestamp`, `level`, `message`, `meta` - Метод `logRequest(method, path, body, response, duration)` — логировать каждый запрос - Метод `logError(error, context)` — логировать ошибки #### `FileStorageService` Простой сервис для работы с JSON-файлами: - `read(filepath): T | null` - `write(filepath, data: T): void` - Все операции синхронные (`fs.readFileSync` / `writeFileSync`) - Автосоздание директории если не существует #### `MetricsService` Хранить данные в `data/metrics.json`: ```json { "total": 0, "byCategory": { "question": 0, "partnership": 0, "job": 0, "spam": 0, "other": 0 }, "bySentiment": { "positive": 0, "neutral": 0, "negative": 0 }, "lastUpdated": "ISO timestamp" } ``` `MetricsController` → `GET /api/metrics` → вернуть текущие метрики #### `HealthController` → `GET /api/health` ```json { "status": "ok", "timestamp": "ISO", "uptime": 12345, "version": "1.0.0" } ``` #### Rate Limiting `ThrottlerModule` с настройками из env: `ttl: THROTTLE_TTL (60000ms)`, `limit: THROTTLE_LIMIT (5)`. Применять `ThrottlerGuard` к `POST /api/contact`. При превышении лимита — NestJS вернёт 429 автоматически. #### Глобальный error handler Создать `AllExceptionsFilter` (`@Catch()`): - Логировать все необработанные ошибки через `LoggerService` - Возвращать единообразный JSON: `{ statusCode, message, timestamp, path }` - Зарегистрировать глобально в `main.ts` #### Swagger - Доступен по `GET /api/docs` - Все эндпоинты задокументированы с `@ApiOperation`, `@ApiResponse`, `@ApiBody` - DTO с `@ApiProperty` декораторами --- ## FRONTEND ### Технологии - **Framework**: Nuxt 3 - **UI**: `@nuxt/ui` (последняя версия) - **Styling**: Tailwind CSS (через `@nuxt/ui`) + SASS - **Package manager**: pnpm - **Lint**: `@antfu/eslint-config` - **HTTP**: встроенный `$fetch` / `useFetch` Nuxt ### `nuxt.config.ts` ```typescript export default defineNuxtConfig({ modules: ["@nuxt/ui"], compatibilityDate: "2024-11-01", devtools: { enabled: true }, css: ["~/assets/styles/main.scss"], runtimeConfig: { public: { apiBase: process.env.NUXT_PUBLIC_API_BASE ?? "http://localhost:3001/api", }, }, }); ``` ### Переменные окружения (`.env.example`) ```env NUXT_PUBLIC_API_BASE=http://localhost:3001/api ``` ### Страница (`pages/index.vue`) Лендинг разработчика. Секции по порядку: 1. **HeroSection** — имя, должность, краткое описание, кнопка "Связаться" 2. **AboutSection** — несколько предложений о себе 3. **SkillsSection** — карточки со скиллами (NestJS, Nuxt, TypeScript, etc.) 4. **ProjectsSection** — карточки проектов (можно placeholder данные) 5. **ContactForm** — форма обратной связи Навбар: фиксированный вверху, ссылки-якоря на секции. Плавный скролл через CSS `scroll-behavior: smooth`. ### `ContactForm.vue` Поля формы: Имя (required), Телефон (required), Email (required), Комментарий (textarea, optional). Логика: - Валидация на клиенте перед отправкой (не пустые обязательные поля, формат email) - `POST` на `${apiBase}/contact` - Состояния: `idle → loading → success | error` - При `success`: показать сообщение + `autoReply` от AI если есть - При `error`: показать текст ошибки из ответа - При 429 (rate limit): показать "Слишком много запросов, попробуйте позже" ### `composables/useContact.ts` ```typescript export function useContact() { const config = useRuntimeConfig(); const loading = ref(false); const error = ref(null); const success = ref(false); const aiReply = ref(null); async function submit(data: ContactFormData) { loading.value = true; error.value = null; try { const res = await $fetch(`${config.public.apiBase}/contact`, { method: "POST", body: data, }); success.value = true; aiReply.value = res.ai?.autoReply ?? null; } catch (e: any) { if (e.status === 429) error.value = "Слишком много запросов. Попробуйте через минуту."; else error.value = e.data?.message ?? "Что-то пошло не так"; } finally { loading.value = false; } } return { loading, error, success, aiReply, submit }; } ``` ### Дизайн Тёмная тема (dark mode по умолчанию). Современный минималистичный стиль разработчика. Использовать компоненты `@nuxt/ui`: `UButton`, `UInput`, `UTextarea`, `UCard`, `UBadge`, `UAlert`. Анимации появления секций при скролле через CSS `@keyframes` + `IntersectionObserver`. --- ## Порядок выполнения ### Backend 1. `mkdir backend && cd backend` → `pnpm init` → установить зависимости → scaffold через `nest new . --package-manager pnpm` или вручную 2. Настроить `tsconfig.json`, `.env` из `.env.example` 3. Создать `FileStorageService`, `LoggerService` (базовые утилиты) 4. Создать `AiService` → протестировать вызов Groq 5. Создать `MailService` → протестировать отправку (можно с `SMTP_HOST=smtp.ethereal.email` для теста) 6. Создать `ContactModule`: DTO → Controller → Service (с AI + mail + logging) 7. Создать `MetricsModule` и `HealthController` 8. Настроить `ThrottlerModule`, `AllExceptionsFilter`, Swagger 9. Проверить: `pnpm dev` → `http://localhost:3001/api/docs` открывается ### Frontend 1. `pnpm dlx nuxi@latest init frontend` → выбрать нужные модули 2. Установить `@nuxt/ui`, `sass` 3. Настроить `nuxt.config.ts`, `.env` 4. Создать `useContact` composable 5. Сверстать секции лендинга (Hero → About → Skills → Projects → Contact) 6. Реализовать `ContactForm.vue` с полной логикой 7. Проверить: форма отправляет запрос, показывает AI-ответ --- ## Важные детали - `data/` директория в `backend/` — создавать автоматически при старте если не существует - Метрики и rate-limit данные — хранить в файлах, не в памяти (чтобы переживали рестарт) - Логи — один файл в день, имя формата `requests-2025-01-15.log` - AI fallback — если `GROQ_API_KEY` не задан или запрос упал, ContactService продолжает работу (отправляет письма без AI-анализа), в ответе `ai: null` - Mail fallback — если SMTP не настроен (пустой `SMTP_HOST`), MailService логирует письмо в консоль вместо отправки (для удобства разработки) - CORS — только `FRONTEND_URL` из env, не `*` - Все секреты только через env, никаких хардкодов --- ## Что должно работать в итоге - `http://localhost:3001/api/docs` — Swagger UI со всеми эндпоинтами - `POST http://localhost:3001/api/contact` — принимает форму, анализирует AI, отправляет письма, логирует - `GET http://localhost:3001/api/health` — статус сервиса - `GET http://localhost:3001/api/metrics` — статистика обращений - `http://localhost:3000` — лендинг с формой, которая работает с бэком - Форма показывает AI-автоответ после успешной отправки - Rate limiting: больше 5 запросов в минуту → 429 - Логи пишутся в `backend/data/logs/`