Share This
Связаться со мной
Крути в низ
Categories
//API-клиент на Typescript

API-клиент на Typescript

В этой статье я подробно расскажу о реализации API-клиента на языке TypeScript для работы как со сторонними API, так и со своими собственными. Клиент может работать с публичными и защищенными эндпойнтами и не привязан к конкретному фреймворку, что делает его пригодным для использования в React, Vue, Svelte и других фреймворках.

api klient na typescript e35a811 - API-клиент на Typescript

Создавая приложение сложнее ToDo-листа, чаще всего нам требуется взаимодействовать с какими-то данными, хранящимися на сервере. Это могут быть как прогнозы погоды, обрабатываемые сторонним API, так и данные наших клиентов, будь то их логин и пароль или список покупок в магазине. Работая с SPA (Single Page Application) приложением, нам нужно эти самые данные получать, модифицировать и отправлять со стороны клиента. Следовательно, нужно иметь какую-то прослойку, отвечающую за взаимодействие с сервером. В этой статье рассмотрим использование API-клиента с библиотекой React, хотя ей можно смело пользоваться на том же Vue, Svelte и так далее.

Почему не прописать все запросы в компонентах, где они используются?

Все просто: если у вас поменяется интерфейс API, с которым вы работаете, вам придется пройтись по всему коду и найти все точки изменений, которые это затронуло. Можно попробовать вынести эту логику в React-хуки, раз уж сейчас речь о нем, но это решение не получится использовать в других проектах c другими фреймворками.

Реализация на Typescript

Для начала вынесем домены, где находятся API, в своего рода конфиг, работающий с .env-файлом:

         REACT_APP_API_BASE_URL=http://localhost:8083      
         export default {     get apiBaseUrl(): string {         return process.env.REACT_APP_API_BASE_URL || "";     }, }      

Затем напишем сам абстрактный клиент, не привязанный к данному домену. Для его работы потребуются библиотеки axios и axios-extensions.

Код клиента:

         import axios, {AxiosInstance, AxiosRequestConfig} from "axios"; import {     Forbidden,     HttpError,     Unauthorized } from '../errors'; import {Headers} from "../types";  export class ApiClient {     constructor(         private readonly baseUrl: string,         private readonly headers: Headers,         private readonly authToken: string = ""     ) {}      public async get(endpoint: string = "", params?: any, signal?: AbortSignal): Promise<any> {         try {             const client = this.createClient(params);             const response = await client.get(endpoint, { signal });             return response.data;         } catch (error: any) {             this.handleError(error);         }     }      public async post(endpoint: string = "", data?: any, signal?: AbortSignal): Promise<any> {         try {             const client = this.createClient();             const response = await client.post(endpoint, data, { signal });             return response.data;         } catch (error) {             this.handleError(error);         }     }      public async uploadFile(endpoint: string = "", formData: FormData): Promise<any> {         try {             const client = this.createClient();             const response = await client.post(endpoint, formData, {                 headers: {                     "Content-Type": "multipart/form-data",                 }             })             return response.data;         } catch (error) {             this.handleError(error);         }     }      private createClient(params: object = {}): AxiosInstance {         const config: AxiosRequestConfig = {             baseURL: this.baseUrl,             headers: this.headers,             params: params         }         if (this.authToken) {             config.headers = {                 Authorization: `Bearer ${this.authToken}`,             }         }         return axios.create(config);     }      private handleError(error: any): never {         if (!error.response) {             throw new HttpError(error.message)         } else if (error.response.status === 401) {             throw new Unauthorized(error.response.data);         } else if (error.response.status === 403) {             throw new Forbidden(error.response.data);         } else {             throw error         }     } }      

В клиенте используются пользовательские типы, такие как Headers, который, по сути, является просто словарем [key: string]: string, и различные ошибки, которые наследуют глобальный класс Error (Unauthorized, Forbidden, HttpError), чтобы в дальнейшем было проще понять, что послужило их причиной.

У класса всего три публичных метода, которые при каждом использовании генерируют axios-клиент. Этот клиент может работать как с публичными эндпоинтами API, так и с защищенными, путем добавления заголовка с Bearer-токеном. Как клиент получает этот самый токен, будет рассмотрено позже. Как get-, так и post-методы используют необязательный параметр abortSignal, который позволяет прервать отправку запроса в зависимости от действий пользователя.

В случае с отправкой каких-либо файлов на сервер клиент использует метод uploadFile(), отправляя на сервер запрос с заголовком Content-Type: multipart/form-data.

Для инкапсуляции логики создания этих клиентов, напишем фабрику.

Код фабрики:

         import {Headers} from "../../types"; import {ApiClient} from "../../clients";  export class ApiClientFactory {     constructor(         private readonly baseUrl: string,         private readonly headers: Headers = {}     ) {}      public createClient(): ApiClient {         return new ApiClient(this.baseUrl, this.headers);     }      public createAuthorizedClient(authToken: string): ApiClient {         return new ApiClient(this.baseUrl, this.headers, authToken);     } }      

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

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека фронтендера» Интересно, перейти к каналу

Конкретная реализация

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

         import {ApiClientInterface} from "./clients"; import {Profile} from "./models";  export class ProfileManager {     constructor(private readonly apiClient: ApiClientInterface) {}      public async get(): Promise<Profile> {         return this.apiClient.get("");     } }      

В данном примере нам не важна модель, которую мы используем для профиля. Будем просто считать, что она совместима с передаваемым с сервера значением.

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

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

Код фабрики:

         import {ApiClientFactory} from "./clients"; import {Headers} from "../types"; import {ProfileManager} from "../ProfileManager";  export class ProfileManagerFactory {     private readonly apiClientFactory: ApiClientFactory;      constructor(baseUrl: string, headers: Headers) {         this.apiClientFactory = new ApiClientFactory(             `${baseUrl}/api/v1/profile`,             headers         );     }      public createProfileManager(authToken: string): ProfileManager {         return new ProfileManager(             this.apiClientFactory.createAuthorizedClient(authToken)         );     } }      

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

Dependency injection

Теперь осталось только написать функцию, которая будет отвечать за предоставление рабочего менеджера профилей в любой части кода, будь то React-компонент или независимый Typescript-класс. Выглядеть она будет примерно так:

         export async function createProfileManager(): Promise<apiClient.ProfileManager> {     const factory = new apiClient.ProfileManagerFactory(apiClientConfig.apiBaseUrl, getBaseHeaders());     return factory.createProfileManager(await getAuthToken()); }      

Вначале внутри создается фабрика этих самых менеджеров, в которую передается домен сервера и базовые заголовки, которые выглядят так:

         function getBaseHeaders(): apiClient.Headers {     return {         "Accept-Language": "ru"     } }      

При желании можно добавить на уровне функции создания менеджера любые свои заголовки.

Способ получения API-токена и работы функции getAuthToken() я не буду рассматривать в этой статье, потому что эта тема заслуживает отдельной публикации.

         async function getAuthToken(): Promise<string> {     // Здесь был бы код получения токена, но пока что просто...     return localStorage.getItem("auth-token"); }      

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

Пример работы менеджера профилей представлен ниже:

         useEffect(() => {         (async () => {             try {                 await initProfile();             } catch (error: any) {                 await handleError(error);             } finally {                 setLoading(false);             }         })()     }, []);      const initProfile = async () => {         const manager = await createProfileManager();         const profile = await manager.get();         await dispatch(set(profile));     }      

При запуске функции в хуке useEffect, асинхронно создается менеджер профилей, который затем запрашивает с сервера текущее состояние профиля пользователей. В данном примере мы просто записываем полученное состояние в хранилище Redux, чтобы затем работать с этим профилем, не перезапрашивая каждый раз его с сервера. В случае ошибки работы клиента, запускается функция handleError(), которая в зависимости от рода ошибки, о чем я говорил ранее, выполняет те или иные действия.

Итоги

Данная реализация независима от фреймворка, с которым вы работаете, ее можно использовать даже на нативном JS (TS). В ней можно еще много чего доработать, например добавить паттерн «‎Строитель» для создания API-клиента и передачи в него параметров, abortSignal-ов и прочего, или сделать вариативную систему аутентификации через JWT-токен. Все на ваше усмотрение). В следующей статье расскажу вам про способ получения и работу с API-токенами на клиенте.

***

Материалы по теме

  • ☕ Самоучитель для начинающих: как освоить TypeScript с нуля за 30 минут?
  • ⚛️ 12 советов по внедрению TypeScript в React-приложениях

  • 1 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Связаться со мной
Close