Правильно работаем с чужим API

Привет, на связи Antihype JS. Сегодня разберемся какие подводные камни вас могут ожидать при работе со внешними API и как их можно избежать.

Правильно работаем с чужим API

Сразу к делу, рассмотрим самый обычный код:

type User = { firstName: string lastName: string } async function getUser(): Promise<User> { const res = await fetch('/api/user') // res.json() -> Promise<any> return res.json() } function formatUser(user: User): string { return `${user.firstName} ${user.lastName}` } getUser().then((user) => console.log(formatUser(user)))

Делаем какой-то запрос, получаем какие-то данные и работаем с ними. Тип данных у нас описан, тайпскрипт прекрасно будет выводить нужные нам поля.

Действительно, если вы работаете в команде и это ваш backend, то беспокоиться не о чем. Любые ошибки вы уже отловите на этапе тестирования. Просто описали (а лучше сгенерировали) типы, получили данные и работаем.

А теперь давайте представим, что вы работаете с API другой команды.

Конечно же, другая команда позаботилась о вас и создала OpenAPI спецификацию для своего бэкенда. Вы успешно провели интеграцию с этим API и закрыли вашу задачу.

Спустя N дней, кто-то изменил код бэкенда и этот эндпоинт начал возвращать вместо поля firstName - поле first_name. Как это часто бывает, об изменении формата ответа вам никто не сообщил и уехал с этими изменениями в релиз.

Об этих изменениях вы узнаёте когда поймали ошибку на проде - вместо нормальных имени и фамилии пользователя отображается что-то вроде: “undefined Антихайпов”.

С ужасом зайдя в Swagger, вы обнаруживаете, что ручка возвращает все те же firstName и lastName. Кажется кто-то забыл обновить спеку.

Как же так?! TypeScript ведь не ругается!

Проблема в типе возвращаемого значения метода res.json - это Promise<any>. А если быть точнее, то TypeScript никак не может гарантировать, что внешний мир всегда будет возвращать данные нужного вам типа.

TypeScript компилируется в JavaScript, а работа с данными неизвестной структуры в JS это всегда лотерея, которая приводит к непредсказуемому поведению. В лучшем случае ваш код просто упадет с ошибкой, а в худшем может повлечь финансовые потери у пользователей.

Самым правильным решением в данной ситуации будет использование рантайм проверок данных от сервера.

Каким образом мы можем провалидировать какие-то данные? В языке TypeScript существует концепция type guard. Они позволяют проверить ваши данные на принадлежность какому-то типу.

Например, ручной тайпгард для типа User может выглядеть так:

function isUser(data: unknown): data is User { return ( typeof data === 'object' && data !== null && 'firstName' in data && typeof data.firstName === 'string' && 'lastName' in data && typeof data.lastName === 'string' ) }

На вход получаем неизвестный тип данных. С помощью рантайм проверок мы должны доказать, что параметр data соответствует типу User.

Выглядит громоздко, правда?

На помощь могут прийти библиотеки для описания схем для данных. Одна из самых удобных по мнению редакции - zod. Библиотека позволяет описать схемы для самых разных структур и автоматически вывести тип!

Для нашего объекта User мы опишем схему и выведем тип:

import { z } from 'zod' type User = z.infer<typeof userSchema> const userSchema = z.object({ firstName: z.string(), lastName: z.string(), })

А валидировать ответ от сервера будем с помощью метода схемы parseAsync:

async function getUser(): Promise<User> { const res = await fetch('/api/user') const user = await res.json() return userSchema.parseAsync(user) }

Теперь, если бэкенд вернет неверную структуру данных, то функция getUser упадет с ошибкой.

В zod есть множество встроенных методов для валидации. Описать можно длину строки, обязательность поля, валидацию uuid, email. Также поддерживается работа с массивами и enum.

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

import { z } from 'zod' const userSchema = z.object({ firstName: z.string(), lastName: z.string(), middleName: z.string().optional(), }) const user = userSchema.parse({ firstName: 'a', lastName: 'b' })

Валидация прошла успешно, ведь поле middleName указано как optional.

Но, если передать невалидную структуру:

const user = userSchema.parse({ firstName: 'a' })

То в консоли увидим следующую ошибку валидации от zod:

ZodError: [ { "code": "invalid_type", "expected": "string", "received": "undefined", "path": [ "lastName" ], "message": "Required" } ]

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

Описывать схемы руками - это очень утомительно. Может добавить какую-нибудь автоматику?

Все уже придумали до нас - пакет openapi-zod-client.

Эта библиотека позволяет генерировать полноценный TypeScript http-клиент для вашего АПИ. Для генерации требуется только OpenAPI схема. Клиент валидирует входящие данные с помощью схем zod. Поддерживается работа со спецификациями в YAML и JSON формате. Работа с OpenAPI спеками версии ниже чем 3 не гарантируется, так что будьте осторожными.

Классический пример со схемой PetStore и сгенерированным кодом:

Правильно работаем с чужим API

На выходе готовый typesafe клиент, который можно импортить и использовать. А еще можно настроить публикацию клиента в виде отдельного npm пакета при релизах новых версий бэкенда.

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

Также подпишитесь на наш канал, там мы пишем и общаемся про frontend и все что с ним связано.

2222
3 комментария

Братан, хорош, давай, давай, вперёд! Контент в кайф, можно ещё? Вообще красавчик! Можно вот этого вот почаще?

3
Ответить

Ни разу не паритесь блед

2
Ответить

Под любую статью заходишь и там несколько таких ботов

1
Ответить