Модуль 2.10 · Урок 2
Урок 2: Реши 3 задачи через агента
flowchart TD
A["Финальный проект"] --> B["Задача 1: Баг-фикс"]
A --> C["Задача 2: Новая фича"]
A --> D["Задача 3: Рефакторинг"]
B --> B1["Сложность: простая (30 мин)"]
C --> C1["Сложность: средняя (50 мин)"]
D --> D1["Сложность: высокая (80 мин)"]
Чему вы научитесь
В этом уроке вы решите три реальные задачи разной сложности, используя Claude Code как вашего AI-помощника. Вы поймёте:
- Как формулировать правильный промпт для агента
- Когда агент работает быстро, а когда требуется ручное вмешательство
- Как оценивать качество AI-решений
- Какой стиль работы с AI наиболее эффективен
Результат: три решённые задачи + понимание своего стиля работы с AI.
Содержание
Задача 1: Баг-фикс ([*] простая)
Описание бага
Ваш API возвращает ошибку 500 при отправке пустого тела запроса на endpoint POST /api/items.
Как воспроизвести:
curl -X POST http://localhost:3000/api/items \
-H "Content-Type: application/json" \
-d "{}"
Текущее поведение: ошибка 500 Internal Server Error Ожидаемое поведение: ошибка 400 Bad Request с сообщением валидации
Эталонный промпт для агента
Прочитай файл src/routes/items.js и src/middleware/errorHandler.js.
Проблема: POST /api/items возвращает 500 при пустом теле запроса.
Нужно вернуть 400 с сообщением "Item name is required" вместо этого.
Решай пошагово:
1. Запусти npm test - найди падающий тест
2. Прочитай код route'а, пойми откуда ошибка
3. Добавь валидацию входных данных (проверка на пустоту)
4. Сделай правку в коде
5. Запусти npm test - убедись, что тест проходит
6. Запусти npm run lint - проверь стиль кода
После каждого шага показывай результат.
Процесс решения через агента
Шаг 1: Диагностика
Агент читает файлы → находит, что валидация отсутствует
Шаг 2: Исправление
// src/routes/items.js (было)
router.post('/', async (req, res, next) => {
try {
const item = new Item(req.body);
await item.save();
res.json(item);
} catch (err) {
next(err);
}
});
// src/routes/items.js (стало)
router.post('/', async (req, res, next) => {
try {
const { name, description } = req.body;
// Валидация
if (!name || name.trim() === '') {
return res.status(400).json({
error: 'Item name is required'
});
}
const item = new Item({ name, description });
await item.save();
res.json(item);
} catch (err) {
next(err);
}
});
Шаг 3: Тестирование
npm test
# [+] POST /api/items returns 400 for empty name
# [+] POST /api/items creates item with valid data
npm run lint
# [+] No lint errors
Эталонное решение
Файл: src/routes/items.js
const express = require('express');
const router = express.Router();
const Item = require('../models/Item');
const { validateItem } = require('../utils/validators');
// GET все items
router.get('/', async (req, res, next) => {
try {
const items = await Item.find().limit(100);
res.json(items);
} catch (err) {
next(err);
}
});
// GET по ID
router.get('/:id', async (req, res, next) => {
try {
const item = await Item.findById(req.params.id);
if (!item) {
return res.status(404).json({ error: 'Item not found' });
}
res.json(item);
} catch (err) {
next(err);
}
});
// CREATE новый item
router.post('/', async (req, res, next) => {
try {
const { name, description } = req.body;
// Валидация
if (!name || typeof name !== 'string' || name.trim() === '') {
return res.status(400).json({
error: 'Item name is required and must be a string'
});
}
const item = new Item({
name: name.trim(),
description: description || ''
});
await item.save();
res.status(201).json(item);
} catch (err) {
next(err);
}
});
module.exports = router;
Файл: tests/items.test.js (добавить новый тест)
describe('POST /api/items', () => {
it('returns 400 for empty name', async () => {
const res = await request(app)
.post('/api/items')
.send({});
expect(res.status).toBe(400);
expect(res.body.error).toContain('required');
});
it('returns 400 for empty string name', async () => {
const res = await request(app)
.post('/api/items')
.send({ name: ' ' });
expect(res.status).toBe(400);
});
it('creates item with valid data', async () => {
const res = await request(app)
.post('/api/items')
.send({ name: 'New Item', description: 'Test' });
expect(res.status).toBe(201);
expect(res.body.name).toBe('New Item');
});
});
Метрики решения
| Метрика | Значение |
|---|---|
| Время диагностики | 3 минуты |
| Время исправления кода | 2 минуты |
| Количество итераций | 1 |
| Тесты пройдены | [+] Да |
| Линтер | [+] Пусто |
| Строк кода добавлено | 8 |
| Сложность | Низкая |
Задача 2: Новая фича ([**] средняя)
Описание задачи
Добавить пагинацию к endpoint’у GET /api/items.
Текущее поведение: возвращает все items (может быть 10000+) Нужное поведение: возвращать по 20 items с поддержкой параметров:
?page=1— номер страницы (по умолчанию 1)?limit=20— количество items на странице (по умолчанию 20, макс 100)?sort=created_at— поле для сортировки?order=desc— направление (asc/desc)
Ответ:
{
"data": [ ... ],
"pagination": {
"page": 1,
"limit": 20,
"total": 523,
"pages": 27,
"hasNext": true,
"hasPrev": false
}
}
Эталонный промпт
Задача: добавить пагинацию в GET /api/items
Требования:
- Параметры: page (default 1), limit (default 20, max 100)
- Дополнительно: sort и order для сортировки
- Ответ: { data: [...], pagination: {...} }
- Тест: должен пройти npm test
План решения:
1. Прочитай текущий код GET /api/items
2. Напиши новую версию с пагинацией
3. Добавь тесты для пагинации (3-4 теста)
4. Проверь тесты: npm test
5. Проверь документацию
Используй MongoDB методы .skip() и .limit().
Процесс решения
Решение — файл: src/routes/items.js (обновить метод GET)
// GET все items с пагинацией
router.get('/', async (req, res, next) => {
try {
// Получаем параметры
let page = parseInt(req.query.page, 10) || 1;
let limit = parseInt(req.query.limit, 10) || 20;
const sort = req.query.sort || 'created_at';
const order = req.query.order === 'asc' ? 1 : -1;
// Валидация
page = Math.max(page, 1);
limit = Math.min(Math.max(limit, 1), 100); // min 1, max 100
const skip = (page - 1) * limit;
// Запрос к БД
const [items, total] = await Promise.all([
Item.find()
.sort({ [sort]: order })
.skip(skip)
.limit(limit),
Item.countDocuments()
]);
// Расчет пагинации
const pages = Math.ceil(total / limit);
res.json({
data: items,
pagination: {
page,
limit,
total,
pages,
hasNext: page < pages,
hasPrev: page > 1
}
});
} catch (err) {
next(err);
}
});
Тесты — файл: tests/items.test.js (добавить)
describe('GET /api/items (пагинация)', () => {
beforeEach(async () => {
// Создаем 50 test-items
const items = Array.from({ length: 50 }, (_, i) => ({
name: `Item ${i + 1}`,
created_at: new Date(Date.now() - i * 1000)
}));
await Item.insertMany(items);
});
it('возвращает первую страницу по умолчанию', async () => {
const res = await request(app).get('/api/items');
expect(res.status).toBe(200);
expect(res.body.data.length).toBe(20);
expect(res.body.pagination.page).toBe(1);
expect(res.body.pagination.total).toBe(50);
});
it('поддерживает параметр page', async () => {
const res = await request(app).get('/api/items?page=2');
expect(res.body.pagination.page).toBe(2);
expect(res.body.data[0].name).not.toBe('Item 1');
});
it('поддерживает параметр limit', async () => {
const res = await request(app).get('/api/items?limit=10');
expect(res.body.data.length).toBe(10);
expect(res.body.pagination.limit).toBe(10);
});
it('ограничивает limit максимум 100', async () => {
const res = await request(app).get('/api/items?limit=500');
expect(res.body.pagination.limit).toBe(100);
});
it('поддерживает сортировку', async () => {
const res = await request(app)
.get('/api/items?sort=name&order=asc');
expect(res.body.data[0].name).toBe('Item 1');
});
it('hasNext true когда есть ещё страницы', async () => {
const res = await request(app).get('/api/items?limit=20&page=1');
expect(res.body.pagination.hasNext).toBe(true);
expect(res.body.pagination.hasPrev).toBe(false);
});
it('hasPrev true на странице > 1', async () => {
const res = await request(app).get('/api/items?page=3&limit=10');
expect(res.body.pagination.hasPrev).toBe(true);
});
});
Метрики решения
| Метрика | Значение |
|---|---|
| Время планирования | 5 минут |
| Время кодирования | 10 минут |
| Время тестирования | 5 минут |
| Количество итераций | 2 (забыли обработать limit=0) |
| Тесты пройдены | [+] Да |
| Линтер | [+] Пусто |
| Строк кода добавлено | 40 |
| Строк тестов добавлено | 60 |
| Сложность | Средняя |
| Скорость БД при 10000 items | < 50ms |
Задача 3: Рефакторинг ([***] сложная)
Описание задачи
Монолитный файл разделить на модули с добавлением типизации.
Текущее состояние: файл src/server.js содержит 500+ строк (routes, middleware, конфигурация).
Требуемое состояние:
src/
├── server.js # точка входа (50 строк)
├── config.js # конфигурация
├── routes/
│ ├── index.js # объединение всех route'ов
│ ├── users.js
│ ├── items.js
│ └── health.js # новый route для проверки здоровья
├── middleware/
│ ├── index.js # загрузка всех middleware
│ ├── errorHandler.js
│ ├── logging.js
│ └── cors.js
├── utils/
│ ├── logger.js # логирование
│ └── validators.js # валидаторы
└── types.d.ts # TypeScript типы
Эталонный промпт
Задача: рефакторить src/server.js (монолит) на модули.
Текущая проблема:
- 500+ строк в одном файле
- Сложно найти код
- Нельзя переиспользовать части
План TDD:
1. Напиши новую структуру проекта (не трогай старый код)
2. Создай тесты для каждого модуля
3. Переноси код по частям из server.js
4. Убедись, что каждый тест проходит
5. Удали старый server.js
6. Запусти полный набор тестов: npm test
Требование: не ломать существующие endpoint'ы!
Эталонное решение
Файл: src/config.js (новый)
require('dotenv').config();
module.exports = {
PORT: process.env.PORT || 3000,
NODE_ENV: process.env.NODE_ENV || 'development',
DB_URL: process.env.DB_URL,
CORS_ORIGIN: process.env.CORS_ORIGIN || 'http://localhost:3000',
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
};
Файл: src/utils/logger.js (новый)
const config = require('../config');
const LOG_LEVELS = {
debug: 0,
info: 1,
warn: 2,
error: 3,
};
const currentLevel = LOG_LEVELS[config.LOG_LEVEL];
const logger = {
debug: (msg, data) => {
if (currentLevel <= LOG_LEVELS.debug) {
console.log(`[DEBUG] ${msg}`, data || '');
}
},
info: (msg, data) => {
if (currentLevel <= LOG_LEVELS.info) {
console.log(`[INFO] ${msg}`, data || '');
}
},
warn: (msg, data) => {
console.warn(`[WARN] ${msg}`, data || '');
},
error: (msg, err) => {
console.error(`[ERROR] ${msg}`, err || '');
},
};
module.exports = logger;
Файл: src/middleware/logging.js (новый)
const logger = require('../utils/logger');
const loggingMiddleware = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const status = res.statusCode;
const level = status >= 400 ? 'warn' : 'info';
logger[level](
`${req.method} ${req.path} ${status} ${duration}ms`
);
});
next();
};
module.exports = loggingMiddleware;
Файл: src/middleware/cors.js (новый)
const config = require('../config');
const corsMiddleware = (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', config.CORS_ORIGIN);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
};
module.exports = corsMiddleware;
Файл: src/middleware/errorHandler.js (обновить)
const logger = require('../utils/logger');
const errorHandler = (err, req, res, next) => {
const status = err.status || 500;
const message = err.message || 'Internal Server Error';
logger.error(`${req.method} ${req.path}`, {
status,
message,
stack: err.stack,
});
res.status(status).json({
error: message,
status,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
};
module.exports = errorHandler;
Файл: src/middleware/index.js (новый)
const express = require('express');
const loggingMiddleware = require('./logging');
const corsMiddleware = require('./cors');
const errorHandler = require('./errorHandler');
const setupMiddleware = (app) => {
// Порядок важен!
app.use(express.json());
app.use(corsMiddleware);
app.use(loggingMiddleware);
// errorHandler должен быть в конце
app.use(errorHandler);
};
module.exports = setupMiddleware;
Файл: src/routes/health.js (новый)
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
});
});
module.exports = router;
Файл: src/routes/index.js (новый)
const usersRouter = require('./users');
const itemsRouter = require('./items');
const healthRouter = require('./health');
const setupRoutes = (app) => {
app.use('/api/health', healthRouter);
app.use('/api/users', usersRouter);
app.use('/api/items', itemsRouter);
// 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
};
module.exports = setupRoutes;
Файл: src/server.js (новый, упрощённый)
const express = require('express');
const mongoose = require('mongoose');
const config = require('./config');
const setupMiddleware = require('./middleware');
const setupRoutes = require('./routes');
const logger = require('./utils/logger');
const app = express();
// Middleware
setupMiddleware(app);
// Routes
setupRoutes(app);
// DB Connection
mongoose.connect(config.DB_URL, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => logger.info('DB connected'))
.catch(err => logger.error('DB connection failed', err));
// Start server
const server = app.listen(config.PORT, () => {
logger.info(`Server running on port ${config.PORT}`);
});
module.exports = server;
Файл: src/types.d.ts (новый, TypeScript типы)
export interface PaginationQuery {
page?: number;
limit?: number;
sort?: string;
order?: 'asc' | 'desc';
}
export interface PaginationResponse {
page: number;
limit: number;
total: number;
pages: number;
hasNext: boolean;
hasPrev: boolean;
}
export interface ApiResponse<T> {
data?: T;
error?: string;
pagination?: PaginationResponse;
}
Тесты для рефакторинга
Файл: tests/refactor.test.js (новый)
const request = require('supertest');
const app = require('../src/server');
describe('Рефакторированный сервер', () => {
describe('Структура', () => {
it('все старые endpoint\'ы работают', async () => {
const health = await request(app).get('/api/health');
expect(health.status).toBe(200);
});
});
describe('Middleware', () => {
it('CORS headers присутствуют', async () => {
const res = await request(app).get('/api/health');
expect(res.headers['access-control-allow-origin']).toBeDefined();
});
it('логирование работает (нет ошибок)', async () => {
await request(app).get('/api/health');
// Если есть ошибка в logger - тест упадёт
});
});
describe('Error handling', () => {
it('404 для несуществующего маршрута', async () => {
const res = await request(app).get('/api/nonexistent');
expect(res.status).toBe(404);
expect(res.body.error).toBeDefined();
});
it('errorHandler ловит ошибки', async () => {
const res = await request(app).get('/api/items/invalid-id');
expect(res.status).toBe(400 || 500);
});
});
});
Метрики решения
| Метрика | Значение |
|---|---|
| Время анализа структуры | 10 минут |
| Время рефакторинга | 30 минут |
| Время тестирования | 15 минут |
| Количество итераций | 3 (ошибки в импортах) |
| Старый файл (строк) | 500 |
| Новых файлов создано | 10 |
| Общее количество строк | 520 (немного больше, но читаемо) |
| Сложность циклической (было) | 45 |
| Сложность циклической (стало) | 12 |
| Покрытие тестами | 78% |
| Время запуска сервера | 120ms → 110ms |
Практика: Ваши 3 задачи
Теперь ваш ход. Решите все три задачи в своём проекте:
Чек-лист для каждой задачи
- Постановка задачи ясна и записана
- Написан промпт для агента
- Агент получил промпт через Claude Code
- Агент решил задачу (или вы помогли)
- Все тесты проходят:
npm test [+] - Линтер чист:
npm run lint [+] - Изменения закоммичены:
git commit -m "..." - Заполнена таблица метрик в Уроке 3
Ключевые выводы
Вы научились:
- Диагностировать баги через тесты и логи
- Формулировать задачи для AI-агента
- Разделять задачу на этапы (план → код → тест → проверка)
- Применять TDD (тесты сначала) для рефакторинга
- Оценивать качество решений агента через метрики
- Комбинировать AI и ручную работу для оптимального результата
Следующий шаг: Урок 3, где вы сравните результаты и сделаете выводы.
Помните: даже если агент не решил всё с первой попытки — это нормально. Важно, что он ускорил процесс. Лучший результат получается из взаимодействия человека и AI.
Бонус: командная работа
Если вы учитесь в команде:
- Решите разные задачи и обменяйтесь результатами
- Сравните, как разные люди работают с агентом
- Обсудите, какие промпты сработали лучше
- Создайте своё руководство для команды на основе опыта