Типизация API ответов

Одним из ключевых аспектов разработки современных веб-приложений является строгая типизация данных, особенно при взаимодействии с API. Fresh, как фреймворк на базе Deno и TypeScript, предоставляет мощные инструменты для безопасного описания и проверки структуры ответов от серверных функций и маршрутов.

Основы типизации данных

Fresh использует возможности TypeScript для статической типизации. Любой API-обработчик может возвращать объекты строго определённого типа. Это позволяет:

  • избежать ошибок при обработке данных на клиенте;
  • улучшить автодополнение и проверку кода в редакторе;
  • документировать структуру данных прямо в коде.

Типизация начинается с описания интерфейсов или типов:

interface User {
  id: number;
  name: string;
  email: string;
}

interface ApiResponse<T> {
  success: boolean;
  dat a: T;
  error?: string;
}

Типизация маршрутов API

В Fresh маршруты представляют собой обычные функции, экспортируемые из файлов в папке routes. Каждый маршрут может быть типизирован с использованием обобщённых типов. Например:

import { HandlerContext } FROM "$fresh/server.ts";

export const handler = async (
  req: Request,
  ctx: HandlerContext
): Promise<Response> => {
  const users: User[] = await getUsersFromDb();
  const response: ApiResponse<User[]> = { success: true, data: users };
  return new Response(JSON.stringify(response), {
    headers: { "Content-Type": "application/json" },
  });
};

В этом примере ApiResponse<User[]> гарантирует, что клиент всегда получит объект с полями success и data, где data строго массив пользователей.

Валидация данных на сервере

Хотя TypeScript обеспечивает статическую типизацию, данные, приходящие из внешних источников (например, из базы данных или сторонних API), требуют проверки во время выполнения. Fresh не навязывает конкретную библиотеку для валидации, но интеграция с zod или superstruct является стандартной практикой:

import { z } FROM "zod";

const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

async function getValidatedUsers(): Promise<User[]> {
  const rawUsers = await getUsersFromDb();
  return rawUsers.map(user => userSchema.parse(user));
}

Использование схем позволяет получать ранние ошибки при несоответствии структуры данных и предотвращает баги на клиенте.

Типизация маршрутов с query-параметрами

API часто требует обработки query-параметров. Типизация их значений также повышает надёжность кода.

interface UsersQuery {
  LIMIT?: number;
  offset?: number;
}

export const handler = async (req: Request) => {
  const url = new URL(req.url);
  const query: UsersQuery = {
    LIMIT: url.searchParams.has("limit")
      ? Number(url.searchParams.get("limit"))
      : undefined,
    offset: url.searchParams.has("offset")
      ? Number(url.searchParams.get("offset"))
      : undefined,
  };

  const users = await getUsersFromDb(query.limit, query.offset);
  const response: ApiResponse<User[]> = { success: true, data: users };
  return new Response(JSON.stringify(response), { headers: { "Content-Type": "application/json" } });
};

Тип UsersQuery фиксирует структуру параметров запроса и позволяет безопасно использовать их внутри обработчика.

Типизация ошибок API

API должен корректно обрабатывать ошибки и возвращать их в предсказуемом формате. Для этого создаются отдельные типы:

interface ApiError {
  success: false;
  error: string;
}

function createErrorResponse(message: string): Response {
  const response: ApiError = { success: false, error: message };
  return new Response(JSON.stringify(response), { status: 400, headers: { "Content-Type": "application/json" } });
}

Типизация ошибок позволяет клиенту однозначно понимать структуру ответа и корректно обрабатывать неудачные запросы.

Интеграция с фронтендом

На стороне фронтенда типизация API ответов обеспечивает безопасное использование данных:

async function fetchUsers(): Promise<User[]> {
  const res = await fetch("/api/users");
  const json: ApiResponse<User[]> = await res.json();
  if (!json.success) throw new Error(json.error);
  return json.data;
}

Использование обобщённого типа ApiResponse<User[]> гарантирует, что тип data всегда известен и корректно проверяется TypeScript.

Советы по поддерживаемой типизации

  • Определять отдельные типы для успешного и ошибочного ответа.
  • Использовать обобщения (<T>) для повторно используемых форматов данных.
  • Проверять внешние данные через схемы zod или superstruct.
  • Типизировать все параметры запроса и тела POST-запросов.
  • Поддерживать консистентный формат ответов по всему приложению.

Типизация API в Fresh не ограничивается только компиляцией — она формирует архитектурную основу для безопасного обмена данными между клиентом и сервером, делая код более надёжным и читаемым.