$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогМодели данных и типизация в TypeScript: как проектировать контракт между фронтом и API

Модели данных и типизация в TypeScript: как проектировать контракт между фронтом и API

$ sudo teach IT
·15 сентября 2026 г.·14 мин·104
Модели данных и типизация в TypeScript: как проектировать контракт между фронтом и API

Разберём, как описывать доменные сущности, DTO и error-model на уровне типов, чтобы ловить несовпадения схем до продакшена. Покажем практические шаблоны для типобезопасного маппинга и версионирования контрактов.

Содержание
Почему “просто интерфейс” не спасаетАрхитектура типов: домен, DTO и отображениеДоменные модели и инвариантыDTO как транспортная правдаТипобезопасный маппинг с валидациейКонтракт “запрос/ответ”: типизация на уровне endpointsУнифицированная сигнатура endpoint’аТипы для response union’ов: успех vs ошибкаError-model: типы, которые выдерживают реальные кейсыПроектирование error-model как набора вариантовruntime-парсер error-modelЛовим несовпадения до продакшенаТипобезопасный fetch слой: маппинг HTTP в доменУнифицированная функция запросаВерсионирование контрактов: не ломать фронт каждый релизДва уровня версийВариант: версия DTO как отдельный типEndpoint как “ключ контракта”Встроенное тестирование контрактовDTO ↔ домен: шаблоны маппинга, которые не развалятсяШаблон 1: чистые функции mapXxxШаблон 2: mapping “съедает” различия DTOШаблон 3: “инварианты” должны быть в доменеТипизация входных данных и формы: когда DTO нужен на клиентеРазделите: “формат запроса” и “модель формы”Где брать схемы: OpenAPI, генераторы и “source of truth”Типичные ошибки при моделировании контрактовКонтракт как продукт: как организовать процессВывод: контракт — это граница с нормализованной правдой

В проектах с фронтендом на TypeScript и API на любом языке “контракт” почти всегда существует — но редко описан как контракт на уровне типов. Он живёт в виде документации, примеров запросов в Swagger, ожиданий команды и наборов “проверок на всякий случай”. В итоге расхождения схем всплывают поздно: в лучшем случае на QA, в худшем — в продакшене.

Хорошая новость: TypeScript позволяет описывать доменные сущности, DTO и модели ошибок так, чтобы несовпадения схем ловились до исполнения и хотя бы частично — до деплоя. В этой статье разберём практические подходы: как построить типобезопасные модели, как маппить данные между слоями, как версионировать контракты и как проектировать error-model так, чтобы фронт корректно реагировал на проблемы.

Будем говорить не про “набор интерфейсов ради интерфейсов”, а про проектирование контрактов как части архитектуры.


Почему “просто интерфейс” не спасает

Типизация в TypeScript обычно стартует с чего-то вроде:

code
type UserDto = {
  id: string;
  email: string;
};

и дальше:

code
async function getUser(): Promise<UserDto> {
  const res = await fetch("/api/user/123");
  return res.json();
}

Проблемы начинаются сразу:

  1. Тип “обещает”, но не проверяет. res.json() всегда имеет тип any. Компилятор лишь доверяет разработчику.
  2. Контракт дрейфует. Backend меняет поле email на login, а фронт продолжает компилироваться, потому что никаких проверок реальности нет.
  3. Смешиваются домен и транспорт. На фронте часто хочется “человеческие” модели, но API отдаёт DTO. Если их не разделять, типы становятся хаотичными.
  4. Ошибка — отдельный контракт. Ошибки обычно описаны “как попало”: где-то приходит {message: "..."} , где-то {code: "...", details: ...}. Фронт начинает делать if (typeof err === ...), а это хрупко.

Чтобы бороться с этим, нужно рассматривать типы как модель контракта, включая версионирование и строгие преобразования.


Архитектура типов: домен, DTO и отображение

Хорошая практика — разделить три уровня:

  1. Домен — модели приложения: что это за сущность и как она используется.
  2. DTO — транспортные формы, близкие к JSON API.
  3. Отображение (mapping) — функции, которые преобразуют DTO → домен и домен → DTO (если отправляете данные обратно).

Так вы:

  • делаете контракт “явным” на границе,
  • держите доменные инварианты в домене,
  • а маппинг превращает “сырой JSON” в предсказуемые модели.

Рассмотрим пример: “Заказ” в домене и “OrderDto” на API.

Доменные модели и инварианты

Домен — не просто набор полей. Это место, где обычно живут инварианты и нормализация. Допустим:

  • total в домене всегда положительный number;
  • status — строго из конечного набора.
code
// Domain
export type OrderStatus = "pending" | "paid" | "shipped" | "cancelled";

export type Order = {
  id: string;
  customerId: string;
  itemsCount: number;
  total: number; // всегда >= 0 в домене
  status: OrderStatus;
};

Инварианты (например total >= 0) можно проверять при маппинге.

DTO как транспортная правда

DTO может отличаться:

  • API отдаёт total строкой (как "199.90");
  • статус — в другом регистре: "PAID";
  • itemsCount API может вообще не отдавать — нужно вывести из items.
code
// DTO
export type OrderDto = {
  id: string;
  customer_id: string;
  total_amount: string; // "199.90"
  status: "PENDING" | "PAID" | "SHIPPED" | "CANCELLED";
  items: Array<{ sku: string; qty: number }>;
};

Важно: DTO отражает реальную схему API. Домен — отражает удобство и инварианты приложения.

Типобезопасный маппинг с валидацией

В TypeScript тип сам по себе не валидирует данные. Поэтому маппинг должен либо:

  • использовать runtime-валидацию,
  • либо опираться на генераторы типов из OpenAPI (где валидация/парсинг можно подтянуть),
  • либо хотя бы централизованно ловить несоответствия.

Практичный компромисс: использовать runtime-валидацию через библиотеку вроде zod (или io-ts). Ниже пример с zod.

Вариант: DTO-схема через zod и маппинг в домен

code
import { z } from "zod";

const OrderDtoSchema = z.object({
  id: z.string(),
  customer_id: z.string(),
  total_amount: z.string(), // парсим в domain
  status: z.enum(["PENDING", "PAID", "SHIPPED", "CANCELLED"]),
  items: z.array(
    z.object({
      sku: z.string(),
      qty: z.number().int().nonnegative(),
    })
  ),
});

type OrderDto = z.infer<typeof OrderDtoSchema>;

const statusMap: Record<OrderDto["status"], OrderStatus> = {
  PENDING: "pending",
  PAID: "paid",
  SHIPPED: "shipped",
  CANCELLED: "cancelled",
};

function parseMoneyToNumber(amount: string): number {
  // Пример: "199.90" -> 199.9
  const n = Number(amount);
  if (!Number.isFinite(n)) throw new Error(`Invalid money amount: ${amount}`);
  return n;
}

export function mapOrderDtoToDomain(dto: OrderDto): Order {
  const total = parseMoneyToNumber(dto.total_amount);
  if (total < 0) throw new Error(`Order total must be non-negative`);

  const itemsCount = dto.items.reduce((acc, it) => acc + it.qty, 0);

  return {
    id: dto.id,
    customerId: dto.customer_id,
    itemsCount,
    total,
    status: statusMap[dto.status],
  };
}

Теперь fetch → json → validate → map. Несовпадение схем ловится до того, как данные станут частью домена.

Централизация: маппинг должен быть единым источником правды

Чтобы не размазывать валидацию по проекту, полезно сделать единый “адаптер” для API-ответов:

code
async function getJsonValidated<T>(
  url: string,
  schema: z.ZodSchema<T>,
  init?: RequestInit
): Promise<T> {
  const res = await fetch(url, init);
  const json = await res.json();
  return schema.parse(json);
}

Использование:

code
export async function fetchOrder(orderId: string) {
  const dto = await getJsonValidated(
    `/api/v1/orders/${orderId}`,
    OrderDtoSchema
  );

  return mapOrderDtoToDomain(dto);
}

Так вы гарантируете: домен не получит “грязный” JSON.


Контракт “запрос/ответ”: типизация на уровне endpoints

Дальше логично “упаковать” контракты по endpoint’ам. Это уменьшает риск несоответствий между руками написанными функциями и схемами.

Унифицированная сигнатура endpoint’а

Можно описывать endpoint как “что отправляем” и “что получаем”. Например:

  • GET /orders/:id возвращает OrderDto,
  • ошибки возвращают ErrorModel.
code
type ApiErrorResponse = {
  error: {
    code: string;
    message: string;
    requestId?: string;
    details?: unknown;
  };
};

Но тут важен следующий момент: ошибка — тоже часть контракта и должна быть типизирована более строго, чем code: string.

Мы к этому вернёмся ниже.

Типы для response union’ов: успех vs ошибка

Одно из удобных решений — описать результат как discriminated union:

code
type ApiOk<T> = { ok: true; value: T };
type ApiFail<E> = { ok: false; error: E };

type ApiResult<T, E> = ApiOk<T> | ApiFail<E>;

И endpoint типизируется как Promise<ApiResult<...>>. Например:

code
type GetOrderResponse = ApiResult<Order, ApiError>;

Но тогда где описать ApiError? Нужна error-model.


Error-model: типы, которые выдерживают реальные кейсы

В реальных API есть сложности:

  • 401/403 обычно приходят иначе, чем 400/422,
  • 404 может приходить с {error: {code: "NOT_FOUND"}},
  • 409 — с конфликтом и деталями,
  • валидационные ошибки (422) часто содержат поле-level ошибки.

Если вы делаете “везде одинаковый error: {message}”, вы потеряете возможность правильно обрабатывать ошибки на фронте.

Проектирование error-model как набора вариантов

Рассмотрим кейсы:

  • NOT_FOUND — ресурс отсутствует,
  • UNAUTHORIZED — токен/сессия невалидна,
  • VALIDATION_ERROR — ошибки полей,
  • CONFLICT — бизнес-конфликт (например, заказ уже оплачен).
code
type ApiErrorBase = {
  requestId?: string;
  message: string;
};

type NotFoundError = ApiErrorBase & {
  code: "NOT_FOUND";
  entity?: string;
  id?: string;
};

type UnauthorizedError = ApiErrorBase & {
  code: "UNAUTHORIZED";
};

type ValidationError = ApiErrorBase & {
  code: "VALIDATION_ERROR";
  fieldErrors: Record<string, string>; // "email": "invalid format"
};

type ConflictError = ApiErrorBase & {
  code: "CONFLICT";
  conflictWith?: string;
};

type ApiError = NotFoundError | UnauthorizedError | ValidationError | ConflictError;

runtime-парсер error-model

Типы снова требуют runtime-валидации. Пример со zod:

code
import { z } from "zod";

const ErrorBaseSchema = z.object({
  code: z.string(),
  message: z.string(),
  requestId: z.string().optional(),
});

const NotFoundErrorSchema = ErrorBaseSchema.extend({
  code: z.literal("NOT_FOUND"),
  entity: z.string().optional(),
  id: z.string().optional(),
});

const UnauthorizedErrorSchema = ErrorBaseSchema.extend({
  code: z.literal("UNAUTHORIZED"),
});

const ValidationErrorSchema = ErrorBaseSchema.extend({
  code: z.literal("VALIDATION_ERROR"),
  fieldErrors: z.record(z.string()), // value: string
});

const ConflictErrorSchema = ErrorBaseSchema.extend({
  code: z.literal("CONFLICT"),
  conflictWith: z.string().optional(),
});

const ApiErrorSchema = z.discriminatedUnion("code", [
  NotFoundErrorSchema,
  UnauthorizedErrorSchema,
  ValidationErrorSchema,
  ConflictErrorSchema,
]);

type ApiErrorDto = z.infer<typeof ApiErrorSchema>;

const ApiErrorResponseSchema = z.object({
  error: ApiErrorSchema,
});

Парсер ошибки:

code
async function parseApiError(res: Response): Promise<ApiError> {
  const json = await res.json();
  const parsed = ApiErrorResponseSchema.parse(json);
  return parsed.error;
}

Теперь вы можете гарантировать: ApiError.code — конечный набор, а фронт может делать switch без “ветвлений по наитию”.

Ловим несовпадения до продакшена

Если backend внезапно начинает возвращать code: "NOT_FOUND_WRONG" — парсер упадёт. Это может быть плохо, если не предусмотрена стратегия. Но это как раз то, что хорошо: вы обнаруживаете рассинхрон схем. Дальше — решение:

  • либо логировать и показывать “неизвестная ошибка”,
  • либо падать в development,
  • либо поддерживать fallback schema (но это снижает ценность типизации).

Например, fallback:

code
const UnknownErrorSchema = z.object({
  error: z.object({
    code: z.string(),
    message: z.string(),
    requestId: z.string().optional(),
    details: z.any().optional(),
  }),
});

type UnknownApiError = {
  code: string;
  message: string;
  requestId?: string;
  details?: unknown;
};

async function safeParseApiError(res: Response): Promise<ApiError | UnknownApiError> {
  const json = await res.json();
  try {
    return ApiErrorResponseSchema.parse(json).error;
  } catch {
    const parsed = UnknownErrorSchema.parse(json);
    return parsed.error;
  }
}

Типобезопасный fetch слой: маппинг HTTP в домен

Типизация контрактов становится действительно полезной только тогда, когда она закреплена в вашем HTTP-слое.

Унифицированная функция запроса

Идея:

  • вы передаёте endpoint-конфигурацию,
  • указываете schema для success DTO,
  • и schema для error-model,
  • функция возвращает ApiResult<Domain, ApiError>.

Пример:

code
import { z } from "zod";

type ApiOk<T> = { ok: true; value: T };
type ApiFail<E> = { ok: false; error: E };
type ApiResult<T, E> = ApiOk<T> | ApiFail<E>;

async function requestWithMapping<TDto, TDomain>(args: {
  url: string;
  method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
  dtoSchema: z.ZodSchema<TDto>;
  mapDtoToDomain: (dto: TDto) => TDomain;
  errorParser: (res: Response) => Promise<any>;
  init?: RequestInit;
}): Promise<ApiResult<TDomain, any>> {
  const { url, method = "GET", dtoSchema, mapDtoToDomain, init, errorParser } = args;

  const res = await fetch(url, { method, ...init });

  if (!res.ok) {
    const error = await errorParser(res);
    return { ok: false, error };
  }

  const json = await res.json();
  const dto = dtoSchema.parse(json);
  return { ok: true, value: mapDtoToDomain(dto) };
}

Использование для Order:

code
export async function fetchOrder(orderId: string) {
  return requestWithMapping<OrderDto, Order>({
    url: `/api/v1/orders/${orderId}`,
    dtoSchema: OrderDtoSchema,
    mapDtoToDomain: mapOrderDtoToDomain,
    errorParser: safeParseApiError,
  });
}

Теперь контракт стал “программируемым”: success и error проходят через схемы и маппинг.


Версионирование контрактов: не ломать фронт каждый релиз

Версионирование — это зона, где чаще всего “контракт в документации” выглядит как декларация, а не как механизм.

Два уровня версий

Обычно есть две разные стратегии:

  1. Версия API в URL (/api/v1/...).
  2. Версия схемы внутри payload (например, schemaVersion в данных).

Если вы не можете управлять URL (например, API уже живёт), иногда схема в payload — единственный рычаг.

Вариант: версия DTO как отдельный тип

Допустим, в v1 статус приходит “нижним регистром”, а в v2 — “верхним”. Тогда не пытайтесь сделать один OrderDto на все версии: вы потеряете корректность.

Создайте разные DTO и разные схемы:

code
// v1 DTO
type OrderDtoV1 = {
  id: string;
  customer_id: string;
  total_amount: string;
  status: "pending" | "paid" | "shipped" | "cancelled";
  items: Array<{ sku: string; qty: number }>;
};

// v2 DTO
type OrderDtoV2 = {
  id: string;
  customer_id: string;
  total_amount: string;
  status: "PENDING" | "PAID" | "SHIPPED" | "CANCELLED";
  items: Array<{ sku: string; qty: number }>;
  currency: "EUR" | "USD";
};

Маппинг в один и тот же домен:

code
const statusMapV1 = {
  pending: "pending",
  paid: "paid",
  shipped: "shipped",
  cancelled: "cancelled",
} as const;

const statusMapV2 = {
  PENDING: "pending",
  PAID: "paid",
  SHIPPED: "shipped",
  CANCELLED: "cancelled",
} as const;

Endpoint как “ключ контракта”

Вы можете оформить это как типизированный endpoint-ключ. Например:

code
type ApiVersion = "v1" | "v2";

type GetOrderEndpoint = {
  version: ApiVersion;
  orderId: string;
};

А дальше — выбор нужного парсера по версии:

code
export async function fetchOrderByVersion(args: GetOrderEndpoint) {
  if (args.version === "v1") {
    const res = await requestWithMapping<OrderDtoV1, Order>({
      url: `/api/v1/orders/${args.orderId}`,
      dtoSchema: OrderDtoV1Schema,
      mapDtoToDomain: mapOrderDtoV1ToDomain,
      errorParser: safeParseApiError,
    });
    return res;
  }

  const res = await requestWithMapping<OrderDtoV2, Order>({
    url: `/api/v2/orders/${args.orderId}`,
    dtoSchema: OrderDtoV2Schema,
    mapDtoToDomain: mapOrderDtoV2ToDomain,
    errorParser: safeParseApiError,
  });
  return res;
}

Так вы сохраняете совместимость и не “перемешиваете” версии в одном типе.

Встроенное тестирование контрактов

Чтобы несовпадения ловились раньше, полезно добавить контрактные тесты (контракт → схема → маппинг). Даже простой smoke-test на примерах payload поможет:

  • если API поменял поле — тест падает,
  • если формат статуса поменялся — тест падает,
  • если ошибка стала другой — тест падает.

На практике это часто делается через “fixtures” и прогон валидатора.


DTO ↔ домен: шаблоны маппинга, которые не развалятся

Одна из ошибок — писать “ручные” преобразования полей прямо в компонентах. Лучше выделить слой адаптеров.

Шаблон 1: чистые функции mapXxx

code
export function mapMoneyStringToNumber(s: string): number {
  const n = Number(s);
  if (!Number.isFinite(n)) throw new Error(`Invalid money: ${s}`);
  return n;
}

И затем использовать это в нескольких маппингах.

Шаблон 2: mapping “съедает” различия DTO

  • парсинг дат,
  • преобразование регистров,
  • переименование snake_case → camelCase,
  • вычисление производных полей,
  • нормализация пустых значений.

Пример даты:

code
function parseIsoDateToMs(date: string): number {
  const t = Date.parse(date);
  if (!Number.isFinite(t)) throw new Error(`Invalid date: ${date}`);
  return t;
}

И дальше домен использует number (timestamp) или Date, в зависимости от архитектуры.

Шаблон 3: “инварианты” должны быть в домене

Не превращайте домен в набор string | null “потому что API так делает”. Если API отдаёт null, это должно учитываться в маппинге:

code
function mapOptionalStringToNonEmpty(value: string | null): string {
  if (value == null) throw new Error("Field is required in domain");
  const trimmed = value.trim();
  if (!trimmed) throw new Error("Field must be non-empty");
  return trimmed;
}

Типизация входных данных и формы: когда DTO нужен на клиенте

Контракт — это не только “ответы”. Запросы тоже стоит типизировать.

Разделите: “формат запроса” и “модель формы”

  • Форма UI может хранить строки ввода (например, total как "19.90"),
  • DTO для API — может требовать другой формат (total_amount как строка или число),
  • Домен — хранит нормализованную структуру.

Типизация полезна, когда маппинг запроса тоже валидирует.

Пример: отправка формы оплаты.

code
type PaymentFormState = {
  orderId: string;
  cardNumber: string; // ввод всегда строка
  amountInput: string; // "19.90"
};

type CreatePaymentDto = {
  order_id: string;
  card_number: string;
  amount_amount: string;
};

function mapFormToDto(form: PaymentFormState): CreatePaymentDto {
  // Валидацию можно вынести в zod, но для примера ограничимся парсингом:
  const n = Number(form.amountInput);
  if (!Number.isFinite(n) || n <= 0) throw new Error("Amount must be > 0");

  // Отправляем в DTO строкой “как требует API”
  return {
    order_id: form.orderId,
    card_number: form.cardNumber.replace(/\s+/g, ""),
    amount_amount: form.amountInput,
  };
}

На практике лучше использовать схему zod для формы/DTO, чтобы ошибки отображались у пользователя корректно (а не падали в консоли).


Где брать схемы: OpenAPI, генераторы и “source of truth”

Типизация не возникает из воздуха. Важно определить, что является источником правды:

  • OpenAPI/Swagger документация,
  • Schema в проекте (zod/io-ts),
  • Proto/GraphQL схема,
  • hand-written DTO + runtime schemas.

Если OpenAPI — источник правды, то идеальная цель: генерировать типы и, желательно, парсеры. Но реальность такая: генерация часто даёт “type-only” информацию без runtime-проверок.

Если вы используете zod (или похожие схемы) как источник правды фронта, то контракт становится двусторонним: вы проверяете ответы, а также формируете запросы в ожидаемом формате. Минус — схема может расходиться с backend, если не синхронизировать процессы.

Выбор зависит от команды и зрелости процесса. Однако в любом случае полезно придерживаться принципа:

Граница (DTO и error-model) должна иметь runtime-проверку или хотя бы единую проверяемую схему.


Типичные ошибки при моделировании контрактов

  1. “Схема на бумаге” без runtime-валидации. Typescript не проверяет JSON. Если вы не валидируете — вы просто перенесли проблему в compile-time, где она не работает.
  2. Смешивание DTO и домена. Это усложняет маппинг и делает доменные инварианты недостижимыми.
  3. Нет discriminated union для ошибок. Если ошибки не имеют code-дискриминатора или типы не унифицированы, фронт будет работать через строки и условные проверки.
  4. Одинаковый DTO для разных версий. Это скрывает изменения и ломает типизацию “исподтишка”.
  5. Локальная валидация в компонентах. В итоге валидаторы дублируются, и вы теряете единый контрактный слой.
  6. Отсутствие контрактных тестов. Схемы меняются, и без тестов вы узнаете об этом только при реальном запросе к API.

Контракт как продукт: как организовать процесс

Технически всё описано выше, но остаётся организационный момент. Контракты живут, а значит нужен процесс:

  • backend публикует изменения схем (желательно в OpenAPI),
  • фронтенд обновляет DTO-схемы,
  • контрактные тесты гоняются на CI,
  • error-model и success DTO обновляются вместе,
  • версия API отражается и в URL, и в типах.

Если вы хотите углубиться в практики типобезопасного построения контрактов, в экосистеме TypeScript есть разные подходы — от генерации схем до “ручного” runtime-подхода. Как один из вариантов структурировать эти знания можно посмотреть курс, но ключ — внедрить методологию в ваш код: слой границы, схемы, маппинг и контрактые тесты.


Вывод: контракт — это граница с нормализованной правдой

Типизация в TypeScript не заменяет валидацию, но резко снижает вероятность рассинхрона, если вы делаете следующее:

  • Разносите домен, DTO и error-model по слоям.
  • Делаете маппинг DTO → домен (и при необходимости обратно) чистыми функциями.
  • Используете runtime-валидацию схем на границе, чтобы JSON не становился “ложной типобезопасностью”.
  • Проектируете ошибки как discriminated union по code, чтобы фронт мог корректно реагировать.
  • Версионируете контракты так, чтобы изменения не превращались в “try-catch по всей кодовой базе”.
  • Добавляете контрактные тесты и держите source of truth понятным.

В сумме это превращает контракт между фронтом и API из “договорённости в тикете” в работающую архитектурную часть системы — и даёт шанс ловить несовпадения схем намного раньше, чем они затронут пользователей.

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

Работа с Gantt-логикой задач: как проектировать дедлайны, ретраи и ограничения в асинхронных сервисах
работа

Работа с Gantt-логикой задач: как проектировать дедлайны, ретраи и ограничения в асинхронных сервисах

Покажем практический шаблон управления временем выполнения: дедлайн на весь запрос, отмена вложенных задач, ретраи с backoff и защита от каскадных зависаний при сетевых ошибках.

17 сентября 2026 г.
1010
TypeScript vs JavaScript: когда типы спасают проект, а когда мешают
typescript

TypeScript vs JavaScript: когда типы спасают проект, а когда мешают

Сравниваем подходы на реальных примерах кода, разбираем, при каких размерах и архитектурах проекта TypeScript окупается, а когда достаточно JSDoc.

16 июля 2026 г.
730
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Почему “просто интерфейс” не спасаетАрхитектура типов: домен, DTO и отображениеДоменные модели и инвариантыDTO как транспортная правдаТипобезопасный маппинг с валидациейКонтракт “запрос/ответ”: типизация на уровне endpointsУнифицированная сигнатура endpoint’аТипы для response union’ов: успех vs ошибкаError-model: типы, которые выдерживают реальные кейсыПроектирование error-model как набора вариантовruntime-парсер error-modelЛовим несовпадения до продакшенаТипобезопасный fetch слой: маппинг HTTP в доменУнифицированная функция запросаВерсионирование контрактов: не ломать фронт каждый релизДва уровня версийВариант: версия DTO как отдельный типEndpoint как “ключ контракта”Встроенное тестирование контрактовDTO ↔ домен: шаблоны маппинга, которые не развалятсяШаблон 1: чистые функции mapXxxШаблон 2: mapping “съедает” различия DTOШаблон 3: “инварианты” должны быть в доменеТипизация входных данных и формы: когда DTO нужен на клиентеРазделите: “формат запроса” и “модель формы”Где брать схемы: OpenAPI, генераторы и “source of truth”Типичные ошибки при моделировании контрактовКонтракт как продукт: как организовать процессВывод: контракт — это граница с нормализованной правдой