17 KiB
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):
- Логировать входящий запрос через
LoggerService(в файл) - Вызвать
aiService.analyzeContact(dto)— получить{ sentiment, category, autoReply }. Если упал —null, продолжить - Вызвать
mailService.sendOwnerNotification(dto, aiResult) - Вызвать
mailService.sendUserConfirmation(dto, aiResult) - Обновить метрики через
metricsService.increment(category) - Вернуть результат
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 | nullwrite<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"
}
MetricsController → GET /api/metrics → вернуть текущие метрики
HealthController → GET /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/useFetchNuxt
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)
Лендинг разработчика. Секции по порядку:
- HeroSection — имя, должность, краткое описание, кнопка "Связаться"
- AboutSection — несколько предложений о себе
- SkillsSection — карточки со скиллами (NestJS, Nuxt, TypeScript, etc.)
- ProjectsSection — карточки проектов (можно placeholder данные)
- 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
mkdir backend && cd backend→pnpm init→ установить зависимости → scaffold черезnest new . --package-manager pnpmили вручную- Настроить
tsconfig.json,.envиз.env.example - Создать
FileStorageService,LoggerService(базовые утилиты) - Создать
AiService→ протестировать вызов Groq - Создать
MailService→ протестировать отправку (можно сSMTP_HOST=smtp.ethereal.emailдля теста) - Создать
ContactModule: DTO → Controller → Service (с AI + mail + logging) - Создать
MetricsModuleиHealthController - Настроить
ThrottlerModule,AllExceptionsFilter, Swagger - Проверить:
pnpm dev→http://localhost:3001/api/docsоткрывается
Frontend
pnpm dlx nuxi@latest init frontend→ выбрать нужные модули- Установить
@nuxt/ui,sass - Настроить
nuxt.config.ts,.env - Создать
useContactcomposable - Сверстать секции лендинга (Hero → About → Skills → Projects → Contact)
- Реализовать
ContactForm.vueс полной логикой - Проверить: форма отправляет запрос, показывает 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/