Домашнє завдання #25: CRUD твого каталогу
У ДЗ #23 твій каталог читав дані з чужого API. Тепер він отримає власний бекенд: API routes на Next.js, які створюють, читають, оновлюють і видаляють записи твоєї колекції у JSON-файлі. Це повний цикл, за яким живе будь-який справжній застосунок.
Що ти вже маєш
- Доменна модель — ти описував її інтерфейсами у ДЗ #19: бери типи звідти і адаптуй
- Тема і дані — твій продукт з ДЗ #5 та каталог з ДЗ #23
Приклади нижче — на дефолтній темі (фільмотека FilmShelf), але будуй свій домен: ігри, книги, рецепти, кросівки. Механіка однакова.
Підготовка
npx create-next-app@latest my-catalog-crud --typescript --tailwind --app
cd my-catalog-crud
«База даних» — JSON-файл data/items.json (назви — під свій домен: movies.json, books.json...):
[
{
"id": "1",
"title": "Той, що біжить по лезу 2049",
"category": "Фантастика",
"year": 2017,
"rating": 9,
"tags": ["neo-noir", "деніс вільньов"],
"notes": "Передивитися в кіно, якщо буде реліз",
"createdAt": "2026-01-15T10:00:00Z"
}
]
Обов'язкові поля моделі: id, title (назва), category (категорія/жанр), хоча б одне числове поле (рік, рейтинг, час приготування...) і хоча б одне поле-масив (теги, інгредієнти, платформи, автори...). Решта — на твій смак.
Завдання
1. API Routes (бекенд)
| Метод | URL | Що робить |
|---|---|---|
| GET | /api/items | Список усіх записів |
| GET | /api/items/[id] | Один запис; немає такого — 404 |
| POST | /api/items | Створити запис |
| PUT | /api/items/[id] | Оновити запис |
| DELETE | /api/items/[id] | Видалити запис |
Читання/запис файлу — через fs.readFileSync / fs.writeFileSync.
ID для нових записів — crypto.randomUUID(): вбудовано в Node.js, жодних бібліотек.
Валідація в POST/PUT (правила фіксовані, назви полів — твої):
title— обов'язкове, мінімум 3 символиcategory— обов'язкове- поле-масив — масив, мінімум 1 елемент
- При порушенні — статус 400 з JSON-описом, яке саме поле не так
2. Список записів (головна сторінка)
Server Component, який читає дані та показує:
- Картки: назва, категорія, твої числові поля, кількість елементів у масиві
- Фільтр за категорією та пошук за назвою
- Посилання на сторінку кожного запису
3. Сторінка запису (/items/[id])
- Повна інформація по запису
- Кнопки «Редагувати» і «Видалити»
- Видалення — з підтвердженням (confirm або модалка), після — редірект на головну
4. Форма створення (/items/new)
Client Component з controlled формою:
- Поля під усі частини твоєї моделі; поле-масив — динамічний список (додати/прибрати елемент)
- Валідація на клієнті тими ж правилами, що й в API
fetchPOST при submit → редірект на сторінку нового запису
5. Форма редагування (/items/[id]/edit)
- Та сама форма (спільний компонент!), але заповнена поточними даними
fetchPUT при submit → редірект на сторінку запису
6. Layout та навігація
Header (бренд + «Всі записи», «Додати»), footer, responsive дизайн.
Структура проєкту
src/
├── app/
│ ├── layout.tsx
│ ├── page.tsx # Список
│ ├── items/
│ │ ├── new/page.tsx # Створення
│ │ └── [id]/
│ │ ├── page.tsx # Деталі
│ │ └── edit/page.tsx # Редагування
│ └── api/items/
│ ├── route.ts # GET all, POST
│ └── [id]/route.ts # GET one, PUT, DELETE
├── components/ # ItemCard, ItemForm, DeleteButton
└── types/ # item.ts — модель з ДЗ #19
data/
└── items.json
Як здати
- Це — окремий публічний репозиторій
README.md: що за каталог, як запустити (npm install,npm run dev); env-змінних немає — зазнач це- Мінімум 5 комітів — наприклад: setup + модель, API routes, список, форма створення, редагування + видалення
- У
data/*.json— мінімум 5 записів твоєї реальної колекції (не lorem) - Скріншоти в
screenshots/:desktop.png(список),mobile.png,interaction.png(форма з помилками валідації або підтвердження видалення) .envз ключами — ніколи не в git (тут їх немає, але правило вічне)- Здай посиланням на репозиторій
Критерії оцінювання
| Критерій | Тип | Бали |
|---|---|---|
| API routes: усі 5 ендпоінтів працюють, GET неіснуючого id → 404 | [код] | 25 |
| Валідація в API: POST без title → 400; title з 2 символів → 400; порожній масив → 400; у відповіді — яке поле не так | [код] | 10 |
| Список: Server Component, фільтр за категорією, пошук за назвою | [поведінка] | 15 |
| Сторінка запису: деталі + видалення з підтвердженням + редірект | [поведінка] | 10 |
| Форма створення: controlled, динамічний список для масиву, клієнтська валідація, редірект на новий запис | [поведінка] | 10 |
| Форма редагування: спільний компонент з формою створення, заповнена даними | [поведінка] | 10 |
| Типи: модель домену (на базі ДЗ #19) використана і в API, і в компонентах | [код] | 5 |
| Скріншоти: список з фільтрами, форма, mobile | [скрін] | 10 |
| Якість коду | [код] | 5 |
| Разом | 100 | |
| Бонус: мітка «улюблене» (поле favorite) + фільтр по улюблених | [поведінка] | +5 |
| Бонус: сортування списку (за датою / назвою / числовим полем) | [поведінка] | +5 |
Бонус: generateMetadata для сторінок записів | [код] | +10 |
Підказки
Рівень 1: напрямок
Почни з бекенду і перевір його без фронтенду — курлом або розширенням типу Postman: створи, прочитай, онови, видали запис. Коли API стабільне, фронтенд стає простим. Винеси читання/запис JSON у два хелпери (readItems/writeItems) в окремий файл — обидва route-файли будуть їх імпортувати.
Рівень 2: які інструменти
Route handler: export async function GET() {...}, POST(request: Request) — тіло через await request.json(), відповідь — NextResponse.json(data, { status: 400 }). Динамічний сегмент [id] приходить у другому аргументі ({ params }; у нових версіях Next — await params). Валідацію винеси у функцію validateItem(data): string[] — використаєш і в API, і на клієнті. Редіректи на клієнті — useRouter().push(...) з next/navigation; після мутацій — router.refresh(), щоб Server Component перечитав дані.
Рівень 3: скелет POST-хендлера
const body = await request.json()const errors = validateItem(body); якщоerrors.length > 0→NextResponse.json({ errors }, { status: 400 })const newItem = { ...body, id: crypto.randomUUID(), createdAt: new Date().toISOString() }const items = readItems(); items.push(newItem); writeItems(items)return NextResponse.json(newItem, { status: 201 })- PUT аналогічний: знайти індекс за id (немає → 404), змерджити поля, зберегти; DELETE —
filterі перевірка, що щось реально видалилось
Що далі
Тепер у твого каталогу є і вітрина (ДЗ #23), і власний бекенд. У ДЗ #26 все зійдеться: портфоліо на Vercel, де цей каталог — головний кейс у секції проєктів. Не видаляй цей код!