Files
internetlab-test-task/PROMPT.md
2026-06-22 09:31:44 +03:00

17 KiB
Raw Permalink Blame History

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)

{
  "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)

# 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)

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:

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-ответ от модели:
    {
      "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<T>(filepath): T | null
  • write<T>(filepath, data: T): void
  • Все операции синхронные (fs.readFileSync / writeFileSync)
  • Автосоздание директории если не существует

MetricsService

Хранить данные в data/metrics.json:

{
  "total": 0,
  "byCategory": {
    "question": 0,
    "partnership": 0,
    "job": 0,
    "spam": 0,
    "other": 0
  },
  "bySentiment": { "positive": 0, "neutral": 0, "negative": 0 },
  "lastUpdated": "ISO timestamp"
}

MetricsControllerGET /api/metrics → вернуть текущие метрики

HealthControllerGET /api/health

{
  "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

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)

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

export function useContact() {
  const config = useRuntimeConfig();
  const loading = ref(false);
  const error = ref<string | null>(null);
  const success = ref(false);
  const aiReply = ref<string | null>(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 backendpnpm 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 devhttp://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/