Я работаю старшим фронтенд-разработчиком в it-отделе одного из крупнейших федеральных застройщиков. Специфика разработки в такой непрофильной компании — сроки спускаемые сверху и вообще не имеющие корреляции с реальными ресурсами и возможностями команды. Именно поэтому мы работаем очень быстро, постоянно пытаясь получить (максимум результата)*3 за (минимум времени)/4.

В этих условиях мы делали большие интеграции с headless CMS Directus и непосредственно с бекендом, используя моковые данные на фронте.
Интеграции были большие и быстрые — и вот тут-то и стало видно, что большинство фронтенд-разработчиков не очень понимают, как подготовить интеграцию, чтобы потом было быстро и не больно заменять моки на реальные ответы. В этой статье пойдет речь о таких подходах на фронтенде,

Подготовка к интеграции

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

Что в этом случае делает фронтендер? 

1. Самый простой случай — верстаем компоненты и встраиваем данные в верстку

Фронтендер берет макеты, верстает, встраивая данные строками/числами и т. д. непосредственно в верстку. Очень странно, но даже опытные разработчики используют такой метод. 

Выглядит это так

// page.tsx
import React from 'react';
import { RootComponent } from '@/app/MockExample/1/_components/RootComponent';

export default async function MockExample() {
  return (
    <>
      <h1>Отличное начало</h1>
      <RootComponent />
      <div>Но финал может быть грустным</div>
    </>
  );
}


// RootComponent.tsx
import { NewsList } from '../NewsList';
import styles from './RootComponent.module.scss';

export const RootComponent = () => {
  return (
    <div className={styles.RootComponentContainer}>
      <h2>Новости</h2>
      <div className={styles.dateWrapper}>
        Дата обновления: <span className={styles.date}>20 сентября 2025</span>
      </div>
      <NewsList />
    </div>
  );
};


// NewsList.tsx
import { NewsItem } from '@/app/MockExample/2/_components/NewsItem';
import styles from './NewsList.module.scss';

export const NewsList = () => {
  const data = new Array(5).fill(true);

  return (
    <div className={styles.Component1Container}>
      <div>Новоости предоставлены агетством Rei</div>
      {data.map((_, index) => (
        <NewsItem key={'newsItem'+index} />
      ))}

      <div>Список авторов:</div>

      <ul>
        {data.map((_, index) => (
          <div className={styles.author} key={'author'+index}>
            Киселев Д.К.
          </div>
        ))}
      </ul>
    </div>
  );
};

//NewsItem.tsx
import styles from './NewsItem.module.scss';

export const NewsItem = () => {
  return (
    <div className={styles.NewsItemContainer}>
      <h3>Новость №1</h3>
      <div className={styles.tags}>Hit!</div>
      <h3>Убийство во Восточном экспрессе</h3>
      <div className={styles.date}>1933</div>
      <img src={'./mockImage'} alt={'Обложка'} />
      <div>Комментариев: 5</div>
      <div>Понравилось: 5</div>
    </div>
  );
};

А между тем, минусы у него очень большие:

  • при такой верстке никто не заморачивается делать различные виды данных — делают просто один вариант, соответственно все ошибки не показанных различий состояний останутся на пост-интеграцию

  • есть большие риски при интеграции пропустить какие-то поля и оставить их моками - в особенности если компоненты сложные и разбиты на более мелкие

  • поскольку данные разбросаны по компонентам, у фронтендера нет полной картины, какие данные понадобятся и в какие структуры их лучше организовать

  • нет готового ответа на вопрос бека, какие данные ему тут нужны. А этот ответ бывает очень полезен — т. к. в этом случае у вас есть готовый контракт, который вы отдаете беку. В большинстве случаев бек вернет вам именно его и это минимизирует вашу работу при интеграции.

Плюсов в этом подходе не вижу.

2. Данные заданы объектом в самих компонентах (и корневых, и дочерних)

В этом случае разработчик задает объекты/массивы данных тут же в компоненте и уже их использует в верстке.

// page.tsx
import React from 'react';
import { RootComponent } from '@/app/MockExample/2/_components/RootComponent';

export default async function MockExample() {
  const data = {
    title: 'Отличное начало',
    comment: 'Но финал может быть грустным',
  };

  const { title, comment } = data;

  return (
    <>
      <h1>{title}</h1>
      <RootComponent />
      <div>{comment}</div>
    </>
  );
}

// RootComponent.tsx
import { NewsList } from '../NewsList';
import styles from './RootComponent.module.scss';

const label = 'Дата обновления: ';

export const RootComponent = () => {
  const data = {
    title: 'Новости',
    date: '20 сентября 2025', // частая ошибка ставить в моки конкретное представление даты, а не дату ISO
  };

  const { title, date } = data;

  return (
    <div className={styles.RootComponentContainer}>
      <h2>{title}</h2>
      <div className={styles.dateWrapper}>
        {label}
        <span className={styles.date}>{date}</span>
      </div>
      <NewsList />
    </div>
  );
};


// NewsList.tsx
import { NewsItem } from '@/app/MockExample/2/_components/NewsItem';
import styles from './NewsList.module.scss';

const label = 'Новости предоставлены агентством';

export const NewsList = () => {
  const dataObject = {
    source: 'Rei',
  };

  const data = new Array(5).fill(true);

  const { source } = dataObject;

  return (
    <div className={styles.Component1Container}>
      <div>
        {label} {source}
      </div>
      {data.map((_, index) => (
        <NewsItem key={'newsItem'+index} />
      ))}

      <div>Список авторов:</div>

      <ul>
        {data.map((_, index) => (
          <div className={styles.author} key={'author'+index}>
            Киселев Д.К.
          </div>
        ))}
      </ul>
    </div>
  );
};

// NewsItem.tsx
import styles from './NewsItem.module.scss';

const commentLabel = 'Комментариев:';
const favoritesLabel = 'Понравилось:';

export const NewsItem = () => {
  const data = {
    title: ' Новость №1',
    tag: 'Hit!',
    heading: 'Убийство в Восточном экспрессе',
    image: './mock.webp',
    alt: 'Обложка',
    commentCount: 5,
    favoritesCount: 5,
  };

  const { title, tag, heading, image, commentCount, favoritesCount, alt } = data;

  return (
    <div className={styles.NewsItemContainer}>
      <h3>{title}</h3>
      <div className={styles.tags}>{tag}</div>
      <h3>{heading}</h3>
      <div className={styles.date}>1933</div>
      <img src={image} alt={alt} />
      <div>
        {commentLabel} {commentCount}
      </div>
      <div>
        {favoritesLabel} {favoritesCount}
      </div>
    </div>
  );
};

Минусы

  • обычно, это также один объект — без различных состояний и комбинаций данных

  • при интеграции очень легко оставить замоканными какие-то дочерние компоненты, т. к. моки задаются на месте использования и никак не связаны

  • остается минус отсутствия понимания необходимой полной структуры данных и отсутствия будущего контракта

3. Данные заданы объектом в каком-то корневом компоненте, в дочерние компоненты данные прокинуты пропсами

// page.tsx
import React from 'react';
import { RootComponent } from '@/app/MockExample/3/_components/RootComponent';

export default async function MockExample() {
  const data = {
    title: 'Отличное начало',
    comment: 'Но финал может быть грустным',
    subTitle: 'Новости',
    date: '20 сентября 2025',
    source: 'Rei',
    newsList: [
      {
        title: ' Новость №1',
        tag: 'Hit!',
        heading: 'Убийство в Восточном экспрессе',
        image: './mock.webp',
        alt: 'Обложка',
        commentCount: 5,
        favoritesCount: 5,
      },
    ],
    authorsList: [{ name: 'Киселев Д.К.' }],
  };

  const { title, comment, subTitle, date, source, newsList, authorsList } = data;

  return (
    <>
      <h1>{title}</h1>
      <RootComponent
        title={subTitle}
        date={date}
        source={source}
        newsList={newsList}
        authorsList={authorsList}
      />
      <div>{comment}</div>
    </>
  );
}


// RootComponent.tsx
import { NewsList } from '../NewsList';
import styles from './RootComponent.module.scss';

const label = 'Дата обновления: ';

type Props = {
  source: string;
  title: string;
  date: string;
  newsList: {
    title: string;
    tag: string;
    heading: string;
    image: string;
    alt: string;
    commentCount: number;
    favoritesCount: number;
  }[];
  authorsList: { name: string }[];
};

export const RootComponent = ({ title, date, source, newsList, authorsList }: Props) => {
  return (
    <div className={styles.RootComponentContainer}>
      <h2>{title}</h2>
      <div className={styles.dateWrapper}>
        {label}
        <span className={styles.date}>{date}</span>
      </div>
      <NewsList source={source} newsList={newsList} authorsList={authorsList} />
    </div>
  );
};

// NewsList.tsx
import { NewsItem } from '../NewsItem';
import styles from './NewsList.module.scss';

const label = 'Новости предоставлены агентством';
const authorsLabel = 'Список авторов:';

type Props = {
  source: string;
  newsList: {
    title: string;
    tag: string;
    heading: string;
    image: string;
    alt: string;
    commentCount: number;
    favoritesCount: number;
  }[];
  authorsList: { name: string }[];
};

export const NewsList = ({ newsList, authorsList, source }: Props) => {
  return (
    <div className={styles.Component1Container}>
      <div>
        {label} {source}
      </div>
      {newsList.map((newsItem, index) => (
        <NewsItem key={'newsItem'+index} />
      ))}

      <div>{authorsLabel}</div>

      <ul>
        {authorsList.map(({ name }, index) => (
          <div className={styles.author} key={'author'+index}>
            {name}
          </div>
        ))}
      </ul>
    </div>
  );
};


// NewsItem.tsx
import styles from './NewsItem.module.scss';

const commentLabel = 'Комментариев:';
const favoritesLabel = 'Понравилось:';

type Props = {
  title: string;
  tag: string;
  heading: string;
  image: string;
  alt: string;
  commentCount: number;
  favoritesCount: number;
};

export const NewsItem = ({
  title,
  tag,
  heading,
  image,
  commentCount,
  favoritesCount,
  alt,
}: Props) => {
  return (
    <div className={styles.NewsItemContainer}>
      <h3>{title}</h3>
      <div className={styles.tags}>{tag}</div>
      <h3>{heading}</h3>
      <div className={styles.date}>1933</div>
      <img src={image} alt={alt} />
      <div>
        {commentLabel} {commentCount}
      </div>
      <div>
        {favoritesLabel} {favoritesCount}
      </div>
    </div>
  );
};

Плюсы

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

Минусы

  • обычно это также один объект — без вариаций

  • остается минус отсутствия понимания необходимой структуры данных (т. к. один вариант, как правило полную картину состояний и полей не дает) и отсутствия будущего контракта

  • здесь добавляется обычно такой минус: в дочерних компонентах типы прописывают через примитивы, не привязывая типы к исходному компоненту. В этом случае при интеграции и при изменениях типов или названий полей, вы получаете необходимость поправить их во всей цепочке компонентов. 

4. Создан массив моковых данных, отражающий различные состояния

В этом случае:

  • создаем отдельный файл для моковых данных

    // mock.ts
    import { TMockExample, TNewsItem, TRootComponent } from '@/app/MockExample/4/types';
    
    const baseNewsItem: TNewsItem = {
      title: 'Короткий заголовок',
      tag: 'Есть длинный тег ',
      heading: 'Убийство в Восточном экспрессе',
      image: './mock.webp',
      alt: 'Обложка',
      commentCount: 5,
      favoritesCount: 5,
    };
    
    // моки типизируем
    export const mockNewsList: TNewsItem[] = [
      baseNewsItem,
      {
        ...baseNewsItem,
        tag: undefined, // разные состояния объектов
      },
      {
        ...baseNewsItem,
        image: undefined, // то же
      },
      {
        ...baseNewsItem,
        alt: undefined, // то же
      },
      {
        ...baseNewsItem,
        commentCount: undefined, // то же
      },
      {
        ...baseNewsItem,
        favoritesCount: undefined, // то же
      },
      {
        title: 'Очень длинный заголовок - реально длинный заголовок',
        heading: 'Убийство в Восточном экспрессе может быть значительно длиннее',
      }, // и здесь
    ];
    
    export const rootComponentBase: TRootComponent = {
      title: 'Новости',
      date: '20 сентября 2025',
      news: {
        source: 'Rei',
        newsList: mockNewsList,
        authorsList: [
          { name: 'Киселев Д.К.' },
          { name: 'Киселев-Заболотный-Залесский Д.К.' },
          { name: 'Киселев Д.К.' },
          { name: 'Киселев-Заболотный-Залесский Д.К.' },
          { name: 'Киселев Д.К.' },
          { name: 'Киселев-Заболотный-Залесский Д.К.' },
          { name: 'Киселев Д.К.' },
          { name: 'Киселев-Заболотный-Залесский Д.К.' },
          { name: 'Киселев Д.К.' },
          { name: 'Киселев-Заболотный-Залесский Д.К.' },
        ],
      },
    };
    
    export const rootComponentWithEmptyAuthorsList: TRootComponent = {
      title: 'Новости',
      date: '29 сентября 2025',
      news: {
        source: 'Interfax ',
        newsList: mockNewsList,
      },
    };
    
    export const mockExampleBase: TMockExample = {
      title: 'Отличное начало',
      comment: 'Но финал может быть грустным',
      rootComponentData: rootComponentBase,
    };
    
  • отражаем в объектах все возможные состояния

  • типизируем моки — в типах уже получаем полную картину для формулирования контракта

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

    // page.tsx
    export default async function MockExample() {
      // здесь в дальнейшем будет получен ответ от API
      const { title, comment, rootComponentData } = mockExampleBase;
      //...
    }
  • в дочерних компонентах прописываем типы на основе корневого (это справедливо для не переиспользуемых компонентов — уникальных для этой структуры. Конечно, если дочерние компоненты — ui или переиспользованы в других местах, типы там должны оставаться абстрактными)

    // RootComponent.tsx
    type RootComponentProps = {
      rootComponentData: TMockExample['rootComponentData'];
    };
    
    // NewsList.tsx
    type Props = {
      news: TMockExample['rootComponentData']['news'];
    };
  • создаем истории в storybook на основе каждого элемента массива моков — получаем в сторибуке полную картину состояний для тестировщика, да и для нас самих это очень полезно при разработке

    // RootComponent.stories.tsx
    import type { Meta, StoryObj } from '@storybook/react';
    import { rootComponentBase, rootComponentWithEmptyAuthorsList } from '@/app/MockExample/4/mock';
    import { RootComponent, RootComponentProps } from './RootComponent';
    
    const meta = {
      title: 'Example/RootComponent',
      component: RootComponent,
    } satisfies Meta<RootComponentProps>;
    
    export default meta;
    type Story = StoryObj<typeof meta>;
    
    export const RootComponentBase: Story = {
      args: { rootComponentData: rootComponentBase },
    };
    
    export const RootComponentWithEmptyNewsList: Story = {
      args: { rootComponentData: rootComponentWithEmptyAuthorsList },
    };
    
    
    //NewsItem.stories.tsx
    import type { Meta, StoryObj } from '@storybook/react';
    import { mockNewsList } from '@/app/MockExample/4/mock';
    import { NewsItem, NewsItemProps } from './NewsItem';
    
    const meta = {
      title: 'Example/NewsItem',
      component: NewsItem,
    } satisfies Meta<NewsItemProps>;
    
    export default meta;
    type Story = StoryObj<typeof meta>;
    
    export const NewsItemBase: Story = {
      args: { newsItem: mockNewsList[0] },
    };
    
    export const NewsItemWithoutTag: Story = {
      args: { newsItem: mockNewsList[1] },
    };
    
    export const NewsItemWithoutImage: Story = {
      args: { newsItem: mockNewsList[2] },
    };
    
    export const NewsItemWithoutAlt: Story = {
      args: { newsItem: mockNewsList[3] },
    };
    
    export const NewsItemWithoutComments: Story = {
      args: { newsItem: mockNewsList[4] },
    };
    
    export const NewsItemWithoutFavorites: Story = {
      args: { newsItem: mockNewsList[5] },
    };
    
    export const NewsItemShort: Story = {
      args: { newsItem: mockNewsList[6] },
    };
    
  • опционально, но очень полезно — по каждой истории сторибука автоматом формируется снэпшот/скриншот

Итого получается вот такие компоненты:

// page.tsx
import React from 'react';
import { RootComponent } from './_components/RootComponent';
import { mockExampleBase } from './mock';

export default async function MockExample() {
  // здесь в дальнейшем будет получен ответ от API
  const { title, comment, rootComponentData } = mockExampleBase;

  return (
    <>
      <h1>{title}</h1>
      <RootComponent rootComponentData={rootComponentData} />
      <div>{comment}</div>
    </>
  );
}

// RootComponent.tsx
import { TMockExample } from '@/app/MockExample/4/types';
import { NewsList } from '../NewsList';
import styles from './RootComponent.module.scss';

const label = 'Дата обновления: ';

export type RootComponentProps = {
  // типы наследуются от корневого элемента
  rootComponentData: TMockExample['rootComponentData'];
};

export const RootComponent = ({ rootComponentData }: RootComponentProps) => {
  const { title, date, news } = rootComponentData;
  return (
    <div className={styles.RootComponentContainer}>
      <h2>{title}</h2>
      <div className={styles.dateWrapper}>
        {label}
        <span className={styles.date}>{date}</span>
      </div>
      <NewsList news={news} />
    </div>
  );
};

// NewsList.tsx
import { TMockExample } from '@/app/MockExample/4/types';
import { NewsItem } from '../NewsItem';
import styles from './NewsList.module.scss';

const label = 'Новости предоставлены агентством';
const authorsLabel = 'Список авторов:';

type Props = {
  // типы наследуются от корневого элемента
  news: TMockExample['rootComponentData']['news'];
};

export const NewsList = ({ news }: Props) => {
  const { newsList, authorsList, source } = news;
  return (
    <div className={styles.Component1Container}>
      <div>
        {label} {source}
      </div>
      {newsList.map((newsItem, index) => (
        <NewsItem key={'newsItem'+index} newsItem={newsItem}  />
      ))}

      <div>{authorsLabel}</div>

      <ul>
        {authorsList?.map(({ name }, index) => (
          <div className={styles.author} key={'author'+index}>
            {name}
          </div>
        ))}
      </ul>
    </div>
  );
};

// NewsItem.tsx
import { TNewsItem } from '@/app/MockExample/4/types';
import styles from './NewsItem.module.scss';

const commentLabel = 'Комментариев:';
const favoritesLabel = 'Понравилось:';

export type NewsItemProps = { newsItem: TNewsItem };

export const NewsItem = ({ newsItem }: NewsItemProps) => {
  const { title, tag, heading, image, commentCount, favoritesCount, alt } = newsItem;

  return (
    <div className={styles.NewsItemContainer}>
      <h3>{title}</h3>
      <div className={styles.tags}>{tag}</div>
      <h3>{heading}</h3>
      <div className={styles.date}>1933</div>
      <img src={image} alt={alt} />
      <div>
        {commentLabel} {commentCount}
      </div>
      <div>
        {favoritesLabel} {favoritesCount}
      </div>
    </div>
  );
};

Плюсы

  • есть единая точка получения данных — в случае проблем, будем сначала искать там, а не по мелким компонентам

  • данные типизированы — можем сразу сформулировать контракт с готовыми структурами данных и их типами

  • легко поменять данные

  • легко поменять типы

  • покрыты и сверстаны все состояния

  • если есть снэпшоты/скриншоты - новые компоненты автоматически покрываются тестами на верстку

Минусы

  • выглядит, как-будто надо приложить много усилий. На самом деле — нет. Профиты покрытия состояний и ускорение при интеграции все покрывают.

Ну, и наконец интеграция

Отлично, мы пошли по четвертому варианту, подготовили моки, загрузили все в сторибук, при верстке увидели все состояния, тестировщик проверил верстку по сторибуку. Дальше отдали типы беку (бек сказал нам спасибо), наш эндпойнт готов — и, вуаля, начинаем интеграцию.

Делаем подключение к беку, типизируем ответ.

// page.tsx  
import { RootComponent } from './_components/RootComponent';

export default async function MockExample() {
  const mockExampleResponse = await getMockExample();

  if (mockExampleResponse == null) {
    return;
  }

  const { title, comment, rootComponentData } = mockExampleResponse;

  return (
    <>
      <h1>{title}</h1>
      <RootComponent rootComponentData={rootComponentData} />
      <div>{comment}</div>
    </>
  );
}

// network/mockExample.ts
export const getMockExample = async () => {
  return apiClient
    .get<TMockExampleDTO>('http://localhost:3000/mock-example')
    .then(({ data }) => mapMockExample(data))
    .catch((error) => {
      console.error('***** [getMockExample]', error);
      return undefined;
    });
};

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

Делать этого не стоит, т. к. сущности все же разные и рано или поздно типы начнут расходиться и вы словите кучу проблем. Поэтому мы разделяем типы входящие (TMockExampleDTO) и внутренние (TMockExample), даже если они одинаковы.

// types
export type TMockExamplePageDTO = {
  title: string;
  comment?: string;
};

export type TNewsItemDTO = {
  title: string;
  tag?: string;
  heading: string;
  image?: string;
  alt?: string;
  commentCount?: number;
  favoritesCount?: number;
};

export type TNewsListDTO = {
  source: string;
  newsList: TNewsItemDTO[];
  authorsList?: { name: string }[];
};

export type TRootComponentDTO = {
  title: string;
  date: string;
  news: TNewsListDTO;
};

export type TMockExampleDTO = TMockExamplePageDTO & { rootComponentData: TRootComponentDTO };

Также в момент получения данных почти всегда возникает необходимость какие-то данные слегка преобразовать, развернуть, поменять название поля на camelCase, что-то подставить по умолчанию и т. д. Не стоит этого делать где-то в компонентах, сделайте в точке получения — в маппере (адаптере). Тогда вы всегда будете знать, где искать и это не будет расползаться по компонентам.

// здесь на входе тип бека, на выходе тип фронтенда
function mapMockExample(response: TMockExampleDTO): TMockExample {
  const {
    rootComponentData: {
      news: { newsList, ...newsRest },
      ...rootComponentDataRest
    },
    ...rest
  } = response;

  const mappedNewsList = newsList.map(({ image, ...newsItemRest }) => ({
    image: `${ASSETS_URL}${image}`,
    ...newsItemRest,
  }));

  return {
    ...rest,
    rootComponentData: {
      ...rootComponentDataRest,
      news: {
        ...newsRest,
        newsList: mappedNewsList,
      },
    },
  };
}

Поэтому обычной практикой у нас является установка адаптера после получения ответа от бека и как раз в маппере мы сопоставляем тип ответа и соответствующий ему фронтовый тип компонента. Это очень удобно, т. к. при изменениях бека, достаточно внести в тип бека изменения и тайпскрипт тут же вам скажет, что пора вносить изменения в адаптер. А т. к. в адаптере используется фронтовый тип корневого компонента, то мы сразу получаем полную картину необходимых изменений. 

Приведу пример: бек присылает нам в ответе вместо heading - headingRenamed.

Меняем тип бека

export type TNewsItemDTO = {
  title: string;
  tag?: string;
  headingRenamed: string; // heading -> headingRenamed
  image?: string;
  alt?: string;
  commentCount?: number;
  favoritesCount?: number;
};

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

Теперь меняем фронтовый тип.

export type TNewsItem = {
  title: string;
  tag?: string;
  headingRenamed: string; // здесь меняем
  image?: string;
  alt?: string;
  commentCount?: number;
  favoritesCount?: number;
};

И не руками, а автоматически меняются и пропсы вложенных компонентов и моки, благодаря нашей типизации.

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

Комментарии (5)


  1. north_leshiy
    26.11.2025 19:05

    1. Почему не генерировать типы сразы из open api контракта который вы согласутете с бекендом? С помощью того же kubb. Тогда переделывать гораздо меньше прийдется.

    2. Почему нужно закладывать сложность на расхождение? Здесь у вас процесс диктует архитектуру. В большинстве случаев когда у вас просто связка бек-фронт (1:1), вы можете диктовать бекенду то каким должно быть api, а бекенд должен быть клиентоориентирлванным. API это продукт и он делается под потребности потребителей.

      Имхо грустно когда две команды не могут договорится об общем контракте и нужно еще маперы писать на фронте. Бывает еще BFF засовывают с мапперами.


    1. toohappy
      26.11.2025 19:05

      Плюсану. Как минимум, пока не готов бэк, можно сформировать либо фронту, либо бэку простой OpenAPI схему, прогнать через kubb / orval и т.п. генераторы, чтобы не плодить вручную типы со схемами и клиенсткими запросами.
      Затем по готовности готового бэка, обновить OpenAPI схему, заново сгенерировать клиентский код, получить подстветку в коде, где нужно заменить методы, типы, схемы и т.п. Тем самым исключая рутинные задачи в традиционной фронтенд разработке с обвязкой бэка.


    1. svetultus Автор
      26.11.2025 19:05

      По поводу генерации типов с бека был грустный опыт на предыдущем проекте: проект был большой и динамичный, типы приезжали новые каждый день и мы каждый день начинали с починки сборки. ИМХО это подходит, когда проект уже входит в более спокойную стадию. А здесь у нас просто не та ситуация - несколько источников данных и не всегда они просто в состоянии отдать в наиболее простом виде структуры. В частности в CMS Directus есть ограничения. Не всегда удвется это предсказать


  1. Zukomux
    26.11.2025 19:05

    Да все ваши варианты один сплошной минус. Изобретаете велосипед там, где не надо. Прозрачности разработки нет. Постоянно надо перед выкаткой удалять, либо жонглировать состоянием что использовать моки или реальные сервисы. Код становится мусорным как раз из-за лишних примесей. Как тестировать сервисы тоже неясно. Из логики поста мне для каждого сервиса необходимо написать N вариантов моков для каждого из случаев! Ах да, заголовок же как раз про 200к LOC Отличная идея выжечь команду на первом же проекте!
    А всего-то надо взять и подключить msw в режиме разработки и всё Сервисы работают как и задуманы без лишних включений. Нет лишних мок-непонятных компонентов. Код прозрачный и пишется так как он и должен. Моки живут отдельно от продакшнена и их нет в бандле. Тестирование ускоряется за счёт автогенерации...


    1. svetultus Автор
      26.11.2025 19:05

      Речь не про сервисы, а про фронтовые компоненты.

      Никаких моков в бандле и так нет - они использованы для покрытия тестами разных вариантов компонента. Зачастую достаточно большие компоненты существуют только в сторибуке и дают при разработке полную картину компонента, не заставляя тестировщика разыскивать эти варианты в разных местах сайта. Поэтому мы имеем полностью протестированные компоненты еще до интеграции. И 200000 строк, поверьте, не от моков.

      Чтобы делать автогенерацию, нужно ее от бека получить, а именно этого и нет. И не всегда она так хороша - выше ответ был