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

Задавшись вопросом«как оптимально организовать работу с 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 ) .

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

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