А что имеем сейчас?
Задавшись вопросом«как оптимально организовать работу с API в nuxt 3?», я столкнулся с суровой действительностью: масштабируемых решений не так много, а все как один говорят о Repository Pattern
-
небольшой вводный видео-гайд из оф. доки Nuxt 3
На мой взгляд, у данного подхода есть очевидный минус — много рутинной работы с типизацией и созданию самих методов, их фасовке, поддержанию в актуальном состоянии.
На выручку нам спешит кодогенерация 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.
-
Плюс: Он имеет более глубокую настройку (только посмотрите на количество опциональных параметров) и позволяет самим гибко создавать templates кодогенерации.
Так же, что немало важно для меня — в качестве http-клиента можно выбрать axios ( у меня нет потребности использовать нативный fetch, а изобретать велосипед для отслеживания прогресса upload/download, работы с перехватчиками, таймаутами, сбросом, сигналами и т.п. не хочется ведь уже есть проверенное и надежное решение ) -
Плюс: Все методы уже обернуты в один большой класс, что позволяет удобно манипулировать ими ( ООП — настало твоё время )
-
Минус: С минимальными настройками не получится так круто типизировать ошибки как с прудыдущим решением, но помним, что всё возможно с шаблонами.
Итого при кодогенерации мы получим:
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.
В целом, работать это будет так:
-
Устанавливаем npx команду в пре-коммит хук, и по возможности создаем джобу в нашем пайплайне;
-
Запускаем проверку typescript по всему проекту;
-
Видим typescript errors, отменяем коммит/сбрасываем джобу и идём исправлять. Помимо прочего, при сравнении версий в git мы увидим, что конкретно поменялось, и не придётся постоянно бегать к бекенд-разработчикам за этой информацией.

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