Как стать автором
Обновить

Безопасное взаимодействие с API: от ошибок к стабильности

Уровень сложностиПростой
Время на прочтение6 мин
Количество просмотров530

Каждый frontend-разработчик сталкивался с ошибкой вида TypeError: Cannot read property 'name' of undefined. Это часть целого класса ошибок в JavaScript, возникающих из-за несоответствия фактического формата данных ожидаемому. Расскажу, как избавиться от подобных проблем и добиться стабильности, внедрив три ключевых шага: API-слой, Backend-for-Frontend (BFF) и проверку с помощью Zod.

А в чём проблема?

Мы знаем, что пользовательский ввод нужно проверять, особенно на бэкенде, прежде чем сохранять данные в базу. Но почему многие фронтенд‑разработчики безоговорочно доверяют данным с бэкенда? Если у вас десятки (микро)сервисов, которые могут не знать о конкретном «фронтенде» и не обязаны заботиться о его формате, то откуда уверенность, что приходящие данные всегда будут строго соответствовать ожиданиям?

Некорректные данные могут появиться в любом звене цепочки (front → service A → service B) и породить массу ошибок: от банальной невозможности прочитать несуществующее свойство до сложных непредсказуемых багов. В результате пользователи видят сбои на сайте, а разработчики тратят часы на поиск причин.

Примеры типичных ошибок:

  • TypeError: Cannot read property "name" of undefined

  • TypeError: Cannot read properties of null (reading "age")

  • TypeError: Cannot destructure property "id" of "undefined" as it is undefined

  • TypeError: arr.map is not a function

  • TypeError: num.toFixed is not a function

  • TypeError: users.forEach is not a function

  • TypeError: Invalid value for number

  • RangeError: Invalid time value

Наша история: ошибка, которая изменила всё

В одном проекте было два endpoint'а для данных о продукте: один возвращал объект Product с полем productInfo, а другой — только ProductInfo. Изначально они совпадали по структуре, и мы использовали один адаптер. Всё шло гладко, пока кто‑то не изменил структуру productInfo в одном из endpoint'ов. Ошибка проявлялась только при запросах к определённым продуктам, и неделю никто не мог понять, где корень проблемы. Этот случай заставил нас переосмыслить подход к управлению данными и важность чётких контрактов между сервисами.

Решение проблемы

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


1. API-слой: стандартизация запросов и данных

Вся логика работы с запросами вынесена в отдельные модули на основе Axios:

  • удаление лишних вложенностей;

  • преобразование snake_case в camelCase;

  • унификация структуры данных для фронтенда;

  • централизованная обработка ошибок.

Это даёт предсказуемость: фронтенд получает данные в едином формате и не зависит от особенностей конкретного сервиса.

Пример API-слоя:

//client.js
import axios from 'axios';

const apiClient = axios.create({
  baseURL: '/api',
  headers: { 'Content-Type': 'application/json' },
});

httpClient.defaults.maxRedirects = 0;
httpClient.defaults.timeout = 10000;
httpClient.defaults.httpAgent = new httpAgent({ keepAlive, scheduling: 'fifo', keepAliveMsecs });

apiClient.interceptors.response.use(
  (response) => response.data,
  (error) => {
    if (error.code === 'ECONNABORTED') {
      return Promise.reject(new HttpTimeoutError(error));
    }

    if (error.response) {
      return Promise.reject(new ApiError(error));
    }

    if (error.isAxiosError) {
      return Promise.reject(new HttpClientInternalError(error));
    }

    if (axios.isCancel(error)) {
      return Promise.reject(new HttpAbortedError(error));
    }
  
    return Promise.reject(new HttpUnknownError(error));
  }
);

export default apiClient;
import client from './client';

export const fetchProduct = (id: string) => 
  client({
    method: 'GET',
    url: `/product`,
    params: {
      id
    },
  }).then((data) => ({
    id: data.id,
    name: data.title,
    price: data.price_info.value,
  }))

2. BFF: объединение запросов и подготовка данных для фронтенда

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

  • объединяет данные из нескольких источников;

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

  • сокращает количество запросов с клиента, агрегируя всё в одном месте.

Пример маршрута BFF:

import Fastify from 'fastify';

const app = Fastify();

app.get('/products', async (request, reply) => {
    const {userId, ...productData} = await fetchProduct(request.id);
    const userData = await fetchUserData(userId);

    reply.send({ ...productData, user: userData });
});

//
const fetchProduct = async (id) => 
  client({
    method: 'GET',
    url: `http://localhost/api/product`,
    params: {
      id
    },
  }).then((data) => ({
    id: data.id,
    name: data.title,
    price: data.price_info.value,
    userId: data.user_info.id,
  }));

const fetchUser = async (id) => 
  client({
    method: 'GET',
    url: `http://localhost/api/user`,
    params: {
      id
    },
  }).then((data) => ({
    id: data.id,
    name: data.name,
    preferences: data.preferences.map(({value}) => value),
  }));

app.listen(3000, () => console.log('BFF is running'));

3. Zod: runtime-защита

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

Проверка на уровне BFF гарантирует, что данные, отправляемые на фронтенд, всегда корректны. Это не только повышает стабильность приложения, но и экономит время на устранение багов.

Пример маршрута с проверкой:

import { z } from 'zod';
import fastifyZod from 'fastify-zod';
//...
app.register(fastifyZod);

export const ProductSchema = z.object({
  id: z.string(),
  name: z.string(),
  price: z.number(),
  user: z.object({
    id: z.string(),
    name: z.string(),
    preferences: z.array(z.string())
  })
});

app.get('/product/:id', {
    schema: {
        response: { 200: ProductSchema },
    },
}, async (request, reply) => {
     const {userId, ...productData} = await fetchProduct(request.id);
    const userData = await fetchUserData(userId);

    reply.send({ ...productData, user: userData });
});

Плюс не забывайте про тесты, чтобы исключить риск случайных изменений без обратной совместимости.

Тесты на Fastify, кстати, пишутся довольно просто
import {options} from 'app';

export function build() {
  options.logger = false;

  const app = Fastify(options);

  beforeAll(async () => {
    void app.register(fp(App));
    await app.ready();
  });

  afterAll(() => app.close());

  return app;
}

describe('/api/getProduct', () => {
  it('id is required', async () => {
    expect.assertions(1);

    const res = await app.inject({
      url: '/api/product'
    });

    expect(JSON.parse(res.payload)).toEqual({ code: 'fst-02' });
  });

  it('response is valid', async () => {
    expect.assertions(2);

    jest.spyOn(resource, 'fetchProduct').mockResolvedValue({...productMock, userId: 999});
    jest.spyOn(resource, 'fetchUser').mockResolvedValue(userMock);

    const res = await app.inject({
      url: '/api/product?id=123'
    });

    expect(fetchProduct).toHaveBeenCalledWith('123');
    expect(fetchProduct).toHaveBeenCalledWith(999);
    expect(res.payload).toEqual(JSON.stringify({...productMock, user: userMock}));
  });
});

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

// обновленный fetchProduct.ts (сразу включает и запрос пользователя по product)
import { ProductSchema } from 'bff/routes/getProduct';
import client from './client';

type Product = z.infer<typeof ProductSchema>;

export const fetchProduct = (id: string) => 
  client({
    method: 'GET',
    url: `/product`,
    params: {
      id
    },
  }).then((data: ProductSchema) => data)

Результаты

  1. Меньше багов. Большинство ошибок теперь отлавливаем на этапе обработки данных.

  2. Быстрая разработка. Командам не нужно постоянно уточнять формат данных, так как он зафиксирован в схемах.

  3. Высокая стабильность. Фронтенд не ломается из-за неожиданного изменения данных на бэкенде.

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


Что делать дальше?

Прежде чем внедрять весь комплекс решений, можно начать с небольших шагов:

  • Вынести все запросы в отдельный API-слой, чтобы единообразно обрабатывать ответы и ошибки.

  • Добавить базовую проверку (хотя бы самых критичных данных) с помощью Zod или аналогичных библиотек.

  • Оценить необходимость BFF: если у вас несколько сервисов и сложные трансформации, то BFF может существенно упростить жизнь.

Если у вас уже есть похожий опыт, поделитесь своими находками в комментариях. А если остались вопросы или идеи — смело задавайте, с радостью обсудим!

Теги:
Хабы:
+3
Комментарии2

Публикации

Информация

Сайт
domclick.ru
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия
Представитель
Dangorche