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

Получение данных, проверка JSON и параллельные запросы

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

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

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

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

Источник заявки может находиться за пределами приложения. Тогда между страницей и данными появляется несколько отказов: сеть, HTTP-статус, разбор JSON и неверная структура. Вынесем получение в серверный модуль lib/data.ts, чтобы страница читала понятный договор fetchTickets(), а детали внешнего ответа оставались в одном месте. Отдельное сообщение источника будет получаться независимо и позволит разобрать параллельное начало операций.

Переменная DATA_API_URL является необязательной. Если она отсутствует, функции возвращают публичные учебные данные и соответствующее пояснение. Это явное исходное состояние, а не незаметная подмена отказавшего реального сервиса. Если адрес задан и источник недоступен, функция бросает исключение. Возвращать фикстуру при любой ошибке было бы опасно: диспетчер мог бы принять демонстрационные записи за актуальное состояние.

Договор внешнего источника

Базовый адрес должен заканчиваться косой чертой и предоставлять пути tickets и notice. Конструктор new URL собирает адрес по правилам URL, а не простой конкатенацией строк. Для списка ожидается массив объектов с числовым id, заголовком, описанием, строковым владельцем и одним из двух статусов. Сообщение имеет объект с полем message. Эти структуры описаны в исходнике и не возникают автоматически из типов TypeScript.

После fetch сначала проверяем response.ok. Полученный HTTP-ответ может иметь статус ошибки, даже если сам Promise разрешился нормально. Затем читаем JSON как unknown. Функция isTicket проверяет каждую необходимую величину и ограниченный набор статусов. Только после этого значение рассматривается как Ticket[]. Аннотация результата без такой проверки скрыла бы проблему до попытки отрисовать неверные данные.

Number.isSafeInteger проверяет числовой ключ, а строковые поля подтверждаются отдельно. Проверка не делает содержание заголовка редакционно качественным и не устанавливает право пользователя. Сейчас источник публичный и учебный. Когда появится личная база, получение будет ограничено серверной идентичностью. Валидация формы и авторизация относятся к разным договорам, хотя обе могут остановить передачу результата.

Свежесть и серверная граница

Запросы имеют { cache: "no-store" }. Здесь выбираем новое получение при обслуживании, а не полагаемся на правила кэширования старой версии фреймворка. Такая настройка полезна для понимания источника свежих данных, но не обещает мгновенное обновление чужой системы. Внешний сервис сам может иметь задержку или собственное хранение. Явная политика Next определяет наш участок цепочки.

Модуль импортирует server-only. Страница может использовать его на сервере, а случайный импорт из клиентского провайдера должен обнаружиться как нарушение границы. Адрес источника не передаётся клиенту через NEXT_PUBLIC_. Если позже понадобится серверный токен, он также останется в этом слое. Однако полученные строки, которые страница отрисует или передаст таблице, станут доступными посетителю; серверный импорт не скрывает вывод.

Независимые и зависимые операции

Страница начинает получение заявок и сообщения через Promise.all. Для старта сообщения не требуется результат списка, поэтому последовательное ожидание добавило бы лишнюю зависимость. Обе операции запускаются до общего await; время ответа определяется их готовностью. Не считаем это измеренным ускорением: реальная задержка не наблюдалась, а текущий снимок может работать с быстрыми фикстурами.

Если второй запрос требовал бы id из первого, он был бы зависимым и не мог стартовать с неизвестным значением. Например, сначала нужен список разрешённых заявок, затем специальная подробность конкретного объекта. В таком случае последовательность выражает настоящий смысл. Не заменяйте каждый await параллельным вызовом автоматически. Сначала определите вход каждой операции и установите, какие данные ей нужны.

Promise.all отвергается при отказе одной операции. В нашем договоре страница требует оба результата, поэтому такой выбор соответствует общему состоянию ошибки. Если сообщение необязательно, можно было бы отдельно обработать его отказ или использовать иную границу. Это продуктовая политика: показывать список без сообщения иногда уместно, но скрывать отказ обязательного источника заявок нельзя.

Ожидаемые отказы и результат

Без внешнего адреса ожидаются две учебные заявки и явное пояснение. При корректном договорном API ожидается его набор. Статус 500, неверный JSON и неизвестный статус объекта должны привести к отказу получения, который встретит граница ошибок списка. Неверное значение не становится пустым результатом поиска. Такой результат различает проблему источника и отсутствие подходящих заявок.

После получения поиск по-прежнему работает на серверной странице. Мы сначала проверили договор данных, затем применили пользовательскую выборку к заголовкам. Это удобная последовательность для небольшого массива. При переходе к базе поиск станет частью SQL-выборки, чтобы не отправлять лишние строки клиенту. В текущем состоянии весь источник является учебным, поэтому этот промежуточный шаг не претендует на экономию большого объёма.

Для самостоятельного разбора назовите этап, на котором обнаружится ответ с status: "pending", и этап для HTTP 404. Затем объясните, почему отсутствие настройки отличается от отказа настроенного источника. Эта точность поможет сохранить правдивый интерфейс, когда вместо публичной фикстуры подключим постоянную базу и личные данные.

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

Страница не знает, находится ли базовый источник в памяти или доступен по HTTP: она знает обещанные проверенные поля и отдельное сообщение. Это полезная модульная граница, но не повод скрывать выбор источника от читателя. В отсутствие URL показано явное пояснение фикстуры, а при настроенном отказе возникает ошибка.

Список и сообщение сейчас обязательны для одного результата. Если позже редакция решит показывать список независимо от информационной строки, нужно изменить договор композиции, а не просто проглотить исключение. Изменение политики должно объяснить, какую часть ответа пользователь вправе считать актуальной и какую ещё ожидает.

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

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

lib/data.ts

import "server-only";
import { sampleTickets } from "@/lib/tickets";
import type { Ticket } from "@/lib/tickets";
function isTicket(value: unknown): value is Ticket {
  if (!value || typeof value !== "object") return false;
  const row = value as Record<string, unknown>;
  return Number.isSafeInteger(row.id) && typeof row.title === "string" && typeof row.body === "string" && typeof row.ownerId === "string" && (row.status === "open" || row.status === "closed");
}
export async function fetchTickets(): Promise<Ticket[]> {
  if (!process.env.DATA_API_URL) return sampleTickets;
  const response = await fetch(new URL("tickets", process.env.DATA_API_URL), { cache: "no-store" });
  if (!response.ok) throw new Error("Источник заявок недоступен");
  const value: unknown = await response.json();
  if (!Array.isArray(value) || !value.every(isTicket)) throw new Error("Источник вернул неверную структуру");
  return value;
}
export async function fetchNotice(): Promise<string> {
  if (!process.env.DATA_API_URL) return "Используются публичные учебные данные.";
  const response = await fetch(new URL("notice", process.env.DATA_API_URL), { cache: "no-store" });
  if (!response.ok) throw new Error("Источник сообщения недоступен");
  const value: unknown = await response.json();
  if (!value || typeof value !== "object" || !("message" in value) || typeof value.message !== "string") throw new Error("Неверное сообщение");
  return value.message;
}

app/(work)/tickets/page.tsx

import Link from "next/link";
import Form from "next/form";
import { fetchTickets, fetchNotice } from "@/lib/data";
export default async function Tickets({ searchParams }: { searchParams: Promise<{ q?: string | string[] }> }) {
  const raw = (await searchParams).q;
  const q = (typeof raw === "string" ? raw : "").trim().slice(0, 80);
  const [tickets, notice] = await Promise.all([fetchTickets(), fetchNotice()]);
  const rows = tickets.filter(item => item.title.toLocaleLowerCase("ru").includes(q.toLocaleLowerCase("ru")));
  return <main><h1>Заявки</h1><p>{notice}</p><Form action="/tickets"><label htmlFor="q">Поиск по заголовку</label><input id="q" name="q" defaultValue={q} /><button>Найти</button></Form>{rows.map(ticket => <article key={ticket.id}><Link href={`/tickets/${ticket.id}`}>{ticket.title}</Link><p>{ticket.status}</p></article>)}{!rows.length && <p>Подходящих заявок нет.</p>}</main>;
}

.env.example

DATA_API_URL=
# Внешний API необязателен; базовый адрес должен заканчиваться / и возвращать договорные tickets и notice.

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

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