Перейти к содержанию

HTTP-обработчики, методы и ответы JSON

Оглавление курса · Скачать все состояния проекта.

Учебная задача: Проверить входные данные и связать метод запроса с конкретной операцией.

Входное состояние: Страницы получают данные из учебного модуля. Выходное состояние: Демонстрационные HTTP-адреса показывают GET, POST, PATCH, DELETE и cookie. Продолжаем «Смену» — учебную диспетчерскую заявок мастерской. Нужны основы JavaScript, TypeScript, React и HTTP. Используем App Router, Next 16.4.0, React 19.3.0 и Node.js не ниже 20.9.0. Основные версии закреплены; диапазоны остальных пакетов ещё не разрешены установкой и lockfile отсутствует. Полный каталог lesson-08 в архиве соответствует окончанию этой главы. Выбирайте один снимок целиком: перенос отдельных файлов из будущих этапов меняет его зависимости и правила данных.

Исходники подготовлены редакционным чтением. Пакеты, сервер, сборка, миграции, примеры и внешнее размещение не выполнялись. Ответы страницы и изменения данных ниже являются ожидаемыми, example_execution: not_run.

Страница React — не единственный ответ приложения. Другому клиенту может понадобиться JSON, а операции важно различать по методу HTTP. Создадим демонстрационные обработчики в route.ts: чтение списка, добавление комментария, частичное изменение и удаление. Они используют только учебные данные и не являются готовым открытым API мастерской. После появления рабочей базы временные комментарии будут удалены из последующих состояний.

Файл маршрута экспортирует функции с именами методов. GET читает, POST создаёт, PATCH меняет указанное значение, DELETE удаляет. В одном уровне нельзя одновременно назначить page.tsx и route.ts одному и тому же URL как независимые ответы без отдельной архитектуры. Здесь API находится под /api, а пользовательские страницы под /tickets, поэтому их роли ясно разделены.

Чтение адреса, заголовков и cookie

Обработчик /api/demo получает NextRequest. Поисковое значение читается через request.nextUrl.searchParams, ограничивается длиной и применяется к публичным фикстурам. Возвращаем JSON с найденными строками и предыдущим значением демонстрационной cookie. Доступ к cookies() асинхронный, как и другие современные API текущего запроса. Не переносим старое синхронное обращение в выбранную версию без адаптации.

Cookie устанавливается на ответе, который действительно возвращается клиенту. Создание одного ответа и установка cookie на другом не дали бы нужной связи. Мы задаём HttpOnly, SameSite, путь и срок действия; значение описывает просмотр демонстрационных заявок, а не личность. Наличие cookie не доказывает вход и не даёт права читать рабочие данные. Позже идентичность будет обеспечивать отдельный механизм Clerk.

Заголовок Cache-Control: no-store фиксирует решение для этого изменяющего состояние просмотра ответа. Произвольный заголовок X-Demo-Only объясняет учебное назначение. Заголовок запроса и заголовок ответа выполняют разные направления передачи. Если понадобится перенаправление, отдельный обработчик вызывает redirect к списку. Не отправляем вместе взаимоисключающие представления «JSON результата» и «страница назначения» без ясного договора клиента.

Входной JSON требует проверки

При POST сначала пытаемся прочитать JSON. Неверный синтаксис приводит к 400. Затем функция commentText принимает unknown, проверяет объект и строковое поле, убирает внешние пробелы и ограничивает длину. Семантически неподходящий текст приводит к 422. Такая последовательность отличает невозможность разобрать представление от значения, которое не соответствует правилам. Аннотация TypeScript сама не проверит присланный JSON.

Новый комментарий получает crypto.randomUUID(). Не используем длину массива как следующий ключ: после удаления она могла бы совпасть с уже существующим идентификатором. Возвращаем созданный объект и статус 201. Данные находятся в памяти конкретного процесса. Перезапуск, несколько процессов или разные экземпляры платформы могут давать разные наборы. Этот приём показывает HTTP-договор, а не долговечное хранение.

PATCH получает асинхронные параметры маршрута, находит комментарий по id и меняет только текст. Если объекта нет, возвращается 404 до изменения. DELETE сначала проверяет индекс, затем удаляет один элемент и возвращает 204 без тела. Проверка отрицательного индекса обязательна: splice(-1, 1) удалил бы последний объект и превратил неизвестный ключ в разрушение другой записи.

Кэширование и состояние процесса

Для GET демонстрационных комментариев также задан запрет хранения. Современные Route Handlers не следует автоматически считать кэшированными по правилам старых выпусков Next. Решение зависит от метода, используемых API и явных настроек. В этом уроке выбираем простой договор свежего чтения памяти и не утверждаем, что он работает одинаково с постоянной базой на многопроцессной платформе.

Ошибки возвращаются структурированным JSON с кратким сообщением. Не включаем в него стек или секреты среды. Клиент должен оценить статус до того, как считать тело успешным объектом. Ответ 204, например, не содержит JSON для чтения. Эта деталь важна для будущих форм: успешный HTTP-транспорт и корректная предметная операция связаны, но не являются одним универсальным значением.

Ожидаемый сценарий можно описать так: корректный POST создаёт UUID-комментарий, PATCH известного id изменяет его текст, DELETE удаляет именно его, повторное удаление сообщает об отсутствии. Все действия относятся к текущему учебному процессу. Здесь запросы не отправлялись и ответы не наблюдались. Открытые адреса этого этапа не принимают рабочие обращения клиентов.

Для самостоятельного разбора сравните пустое тело, неверный JSON, короткий текст и неизвестный id. У каждого отказа свой участок цепочки. Затем объясните, почему две серверные копии массива не образуют одну общую базу. Эта граница подготовит переход к PostgreSQL и серверным действиям, где постоянные данные и полномочия уже будут обязательной частью операции.

Проверьте смысл границ

Пустой список комментариев после перезапуска не доказывает, что DELETE удалил все прежние записи: память просто принадлежала другому процессу. По этой причине нельзя использовать данный демонстрационный модуль как журнал редакции. Его назначение — показать связь метода, входа и ответа. Для долговечного результата источник должен иметь самостоятельное постоянное хранение.

Ответ 204 требует особенно внимательного клиента: у него нет тела, поэтому попытка всегда вызвать json станет лишней ошибкой уже после успешного удаления. Сначала определяйте статус и договор операции. Единый helper «всегда читать JSON» подходит только API, который действительно обещает тело у каждого вида ответа.

Полные файлы этого шага

Следующие файлы показаны полностью. Остальные согласованные компоненты и настройки находятся в том же каталоге архива; сохраняйте их вместе при самостоятельном прохождении.

app/api/demo/route.ts

import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import { sampleTickets } from "@/lib/tickets";
export async function GET(request: NextRequest) {
  const q = (request.nextUrl.searchParams.get("q") || "").slice(0, 80).toLowerCase();
  const rows = sampleTickets.filter(item => item.title.toLowerCase().includes(q));
  const store = await cookies();
  const response = NextResponse.json({ rows, lastView: store.get("demo-view")?.value || null });
  response.headers.set("Cache-Control", "no-store");
  response.headers.set("X-Demo-Only", "true");
  response.cookies.set("demo-view", "tickets", { httpOnly: true, sameSite: "lax", path: "/", maxAge: 3600 });
  return response;
}

lib/demo-store.ts

import "server-only";
export type DemoComment = { id: string; text: string };
export const demoComments: DemoComment[] = [];
export function commentText(value: unknown): string | null {
  if (!value || typeof value !== "object" || !("text" in value) || typeof value.text !== "string") return null;
  const text = value.text.trim();
  return text.length >= 3 && text.length <= 300 ? text : null;
}

app/api/demo-comments/route.ts

import { NextResponse } from "next/server";
import { demoComments, commentText } from "@/lib/demo-store";
export function GET() { return NextResponse.json(demoComments, { headers: { "Cache-Control": "no-store" } }); }
export async function POST(request: Request) {
  let input: unknown;
  try { input = await request.json(); } catch { return NextResponse.json({ error: "Неверный JSON" }, { status: 400 }); }
  const text = commentText(input);
  if (!text) return NextResponse.json({ error: "Текст: от 3 до 300 символов" }, { status: 422 });
  const item = { id: crypto.randomUUID(), text };
  demoComments.push(item);
  return NextResponse.json(item, { status: 201 });
}

app/api/demo-comments/[id]/route.ts

import { NextResponse } from "next/server";
import { demoComments, commentText } from "@/lib/demo-store";
type Context = { params: Promise<{ id: string }> };
export async function PATCH(request: Request, context: Context) {
  const { id } = await context.params;
  const item = demoComments.find(row => row.id === id);
  if (!item) return NextResponse.json({ error: "Нет комментария" }, { status: 404 });
  let input: unknown;
  try { input = await request.json(); } catch { return NextResponse.json({ error: "Неверный JSON" }, { status: 400 }); }
  const text = commentText(input);
  if (!text) return NextResponse.json({ error: "Текст: от 3 до 300 символов" }, { status: 422 });
  item.text = text; return NextResponse.json(item);
}
export async function DELETE(_request: Request, context: Context) {
  const { id } = await context.params;
  const index = demoComments.findIndex(row => row.id === id);
  if (index < 0) return NextResponse.json({ error: "Нет комментария" }, { status: 404 });
  demoComments.splice(index, 1); return new Response(null, { status: 204 });
}

app/api/start/route.ts

import { redirect } from "next/navigation";
export function GET() { redirect("/tickets"); }

Документация и исходники

Договор этой главы раскрыт в официальной документации. Сопоставьте её API с выбранной версией и с объяснённым источником каждого значения. Архив «Смены» содержит двадцать полных состояний и условия самостоятельной подготовки. Ожидаемый результат оценивается по текущей главе: поздние проверки владельца, база и внешние ключи не являются неявно выполненными предпосылками раннего учебного состояния.