Вивчай
Домашнє завдання #25 · Next.js: API Routes, CRUD, Server/Client Components
100 балів+20 бонусintermediate

Домашнє завдання #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
  • fetch POST при submit → редірект на сторінку нового запису

5. Форма редагування (/items/[id]/edit)

  • Та сама форма (спільний компонент!), але заповнена поточними даними
  • fetch PUT при 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

Як здати

  1. Це — окремий публічний репозиторій
  2. README.md: що за каталог, як запустити (npm install, npm run dev); env-змінних немає — зазнач це
  3. Мінімум 5 комітів — наприклад: setup + модель, API routes, список, форма створення, редагування + видалення
  4. У data/*.json — мінімум 5 записів твоєї реальної колекції (не lorem)
  5. Скріншоти в screenshots/: desktop.png (список), mobile.png, interaction.png (форма з помилками валідації або підтвердження видалення)
  6. .env з ключами — ніколи не в git (тут їх немає, але правило вічне)
  7. Здай посиланням на репозиторій

Критерії оцінювання

КритерійТипБали
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-хендлера
  1. const body = await request.json()
  2. const errors = validateItem(body); якщо errors.length > 0NextResponse.json({ errors }, { status: 400 })
  3. const newItem = { ...body, id: crypto.randomUUID(), createdAt: new Date().toISOString() }
  4. const items = readItems(); items.push(newItem); writeItems(items)
  5. return NextResponse.json(newItem, { status: 201 })
  6. PUT аналогічний: знайти індекс за id (немає → 404), змерджити поля, зберегти; DELETE — filter і перевірка, що щось реально видалилось

Що далі

Тепер у твого каталогу є і вітрина (ДЗ #23), і власний бекенд. У ДЗ #26 все зійдеться: портфоліо на Vercel, де цей каталог — головний кейс у секції проєктів. Не видаляй цей код!