Как организовать работу с API в Nuxt 3 без шума и пыли

от автора

А что имеем сейчас?

Задавшись вопросом«как оптимально организовать работу с API в nuxt 3?», я столкнулся с суровой действительностью: масштабируемых решений не так много, а все как один говорят о Repository Pattern

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

Как-то не хочется...

Как-то не хочется…

На выручку нам спешит кодогенерация OpenApi. Давайте кратко рассмотрим два инструмента: openapi-typescript и swagger-typescript-api

Openapi-typescript

Проект состоит из двух частей: кодогенерации и fetch-client, который является не обязательным, но позволяет довольно просто использовать сгенерированные типы.

К примеру, в контексте VUE fetch-client используется следующим образом. И здесь мы можем обратить внимание, что данный инструмент идеально подходит для того же Repository Pattern. Как минимум, нам не нужно самостоятельно писать типы.

Неоспоримое преимущество данного инструмента для меня — это максимальная типизация http ответов по всем возможным кодам ( а не только 200 ).
Eсли в OpenAPI есть 404 и 500, то мы с легкостью можем их получить и использовать в дальнейшем ( прощайте пустые алерты из‑за нестандартных и разношерстных ответов с бека )

варианты ответов

варианты ответов

Типы будут выглядеть следующим образом:

type ErrorResponse500 =   paths["/my/endpoint"]["get"]["responses"][500]["content"]["application/json"]["schema"]; type ErrorResponse404 =   paths["/my/endpoint"]["get"]["responses"][404]["content"]["application/json"]["schema"];

Знаю, вы подумали о том, как было бы здорово сделать generic. Но спешу вас огорчить советом из документации:

Хороший fetch-wrapper никогда не должен использовать generics. Они перегружены и приводят к ошибкам!

Swagger-typescript-api

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

  1. Плюс: Он имеет более глубокую настройку (только посмотрите на количество опциональных параметров) и позволяет самим гибко создавать templates кодогенерации.
    Так же, что немало важно для меня — в качестве http-клиента можно выбрать axios ( у меня нет потребности использовать нативный fetch, а изобретать велосипед для отслеживания прогресса upload/download, работы с перехватчиками, таймаутами, сбросом, сигналами и т.п. не хочется ведь уже есть проверенное и надежное решение )

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

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

    Итого при кодогенерации мы получим:

    export interface SerializerServices {   id: number   /** @maxLength 100 */   typeClassify?: string | null   /** @maxLength 100 */   childTypeClassify?: string | null   /** @maxLength 100 */   name?: string   /** @maxLength 100 */   contentExt?: string | null   /** @maxLength 100 */   content?: string | null   extendImg?: any   platforms?: any }  export type ServicesListRetrieveError = Error500Serializer1 | Error500  export interface Error500Serializer1 {   /** @default "qweqeqweqwe" */   detail?: string }  export interface Error500 {   /** @default "babam" */   code?: string }  export class Api<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {   /**    * No description    *    * @tags services    * @name ServicesListRetrieve    * @request GET:/api/v1/services/list/    * @secure    * @response `200` `SerializerServices`    * @response `404` `Error500Serializer1` Internal Server Erro1231r    * @response `500` `Error500` Internal Server Error    */   servicesListRetrieve = (     query?: {       /** ID сервиса */       service_id?: number     },     params: RequestParams = {},   ) =>     this.request<SerializerServices, ServicesListRetrieveError>({       path: `/api/v1/services/list/`,       method: 'GET',       query: query,       secure: true,       format: 'json',       ...params,     }) }

Подключение к Nuxt 3

Исходя из вышеперечисленного, я предпочел swagger-typescript-api.
В package.json добавим и запустим команду ( все флаги простые и лаконично описаны на первой странице документации ).

"scripts": {     "api:generate": "npx swagger-typescript-api -p http://localhost:8000/api-docs/schema/ -o ./api/generated/django -n api-axios-django.ts  --extract-response-error  --extract-enums --axios --unwrap-response-data --modular --responses", }

В nuxt.config.ts runtimeConfig добавим base url и подключим к переменным окружения.

export default defineNuxtConfig({   // где-то тут ваши остальные настройки      runtimeConfig: {     public: {       BACKEND_URL: process.env.BACKEND_URL,     },   },

Создадим плагин для того, чтобы иметь удобный и глобальный способ импортировать наши API-методы, а так же получить доступ к экземпляру nuxt и его runtimeConfig.

// Наш сгенерированный файл от swagger-typescript-api  import { Api } from '@/api/generated/django/Api' import type { AxiosInstance } from 'axios'  export default defineNuxtPlugin((nuxt) => {   // получаем доступ к runtimeConfig nuxt с переменными   const { $config } = nuxt    const generateV1 = () => {     // создаем axios instance и устанавливаем настройки     return new Api({        // !!!       baseURL: $config.public.BACKEND_URL,         // остальные настройки по необходимости       timeout: 60000     })   }    return {     provide: {       apiService: {         v1: generateV1(),       },     },   } }) 

Идём в компонент и используем плагин. Не забывайте и не игнорируйте специальные композиции nuxt при работе с API. Это чрезвычайно важно, особенно при работе c SSR.

<script lang="ts" setup>   const { $apiService } = useNuxtApp()   const { data } = useAsyncData('services/list', () =>     $apiService.v1.servicesListRetrieve({ service_id: 1 }),   ) </script>  <template>     <div>       {{ data }}     </div> </template>

Отлично! У нас всё получилось. Аpi типизировано, так еще и IDE даёт нам удобные подсказки, при этом мы сохранили все фичи Nuxt.

НО что на счет обработки ошибок? Проверим!

// такого сервиса не существует const { $apiService } = useNuxtApp() const { data, status, error, execute } = useAsyncData('services/list', () =>   $apiService.v1.servicesListRetrieve({ service_id: 111111111111111111 }), )

В error получаем:

{     "message": "Request failed with status code 404",     "statusCode": 500 }

Хмм… Путаница с кодами.
statusCode равен 500 , хотя на самом деле он равен 404

Request URL: http://127.0.0.1:8000/api/v1/services/list/?service_id=11111111 Request Method: GET Status Code: 404 Not Found
ответ в браузере

ответ в браузере

К тому же, в response есть сообщение об ошибке, однако, мы его не видим в error у useAsyncData

{     "detail": "Сервиса с таким айди не существует" }

Какое решение? Всё достаточно просто, нам всего лишь нужно обработать промис аксиоса должным образом. И на помощь нам приходят interceptors.

import { Api } from '@/api/generated/django/Api' import type { AxiosInstance } from 'axios'  export default defineNuxtPlugin((nuxt) => {   const { $config } = nuxt    // Добавляем interceptors    const setupDefaultInterceptors = (instance: AxiosInstance) => {     instance.interceptors.response.use(       function (data) {         return Promise.resolve(data)       },       function (error) {         return Promise.reject(error.response)       },     )      return instance   }    const generateV1 = () => {     const api = new Api({ baseURL: $config.public.BACKEND_URL, timeout: 60000 })     setupDefaultInterceptors(api.instance)     return api   }    return {     provide: {       apiService: {         v1: generateV1(),       },     },   } }) 

и в error мы уже получим:

{     "message": "",     "statusCode": 404,     "statusMessage": "Not Found",     "data": {         "detail": "Сервиса с таким айди не существует"     } }

Итог

Данный метод позволяет с минимальными усилиями использовать типизированные методы API в связке с Nuxt.
В целом, работать это будет так:

  1. Устанавливаем npx команду в пре-коммит хук, и по возможности создаем джобу в нашем пайплайне;

  2. Запускаем проверку typescript по всему проекту;

  3. Видим typescript errors, отменяем коммит/сбрасываем джобу и идём исправлять. Помимо прочего, при сравнении версий в git мы увидим, что конкретно поменялось, и не придётся постоянно бегать к бекенд-разработчикам за этой информацией.

Стоит так же понимать, что кодогенерация целиком и полностью полагается на ваш OpenAPI, за который отвечают бекенд-разработчики. И если они по каким-то причинам игнорируют спецификацию и в целом не уделяют этому должного внимания, то вы выстрелите себе в ногу подобным инструментом ( привет // @ts-ignore ) .

Так что не забываем обговаривать ваше решение с коллегами 🙂


ссылка на оригинал статьи https://habr.com/ru/articles/837584/


Комментарии

Добавить комментарий

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