А что имеем сейчас?
Задавшись вопросом«как оптимально организовать работу с 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 ) .
Так что не забываем обговаривать ваше решение с коллегами :-)