- PVSM.RU - https://www.pvsm.ru -
Задавшись вопросом«как оптимально организовать работу с API в nuxt 3?», я столкнулся с суровой действительностью: масштабируемых решений не так много, а все как один говорят о Repository Pattern
VUE Mastery [1]
небольшой вводный видео-гайд [2] из оф. доки Nuxt 3
medium [3]
На мой взгляд, у данного подхода есть очевидный минус - много рутинной работы с типизацией и созданию самих методов, их фасовке, поддержанию в актуальном состоянии.
На выручку нам спешит кодогенерация OpenApi. Давайте кратко рассмотрим два инструмента: openapi-typescript и swagger-typescript-api
Проект состоит из двух частей: кодогенерации [4] и fetch-client [5], который является не обязательным, но позволяет довольно просто использовать сгенерированные типы.
К примеру, в контексте VUE fetch-client используется следующим образом [6]. И здесь мы можем обратить внимание, что данный инструмент идеально подходит для того же 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. Но спешу вас огорчить советом из документации [7]:
Хороший fetch-wrapper никогда не должен использовать generics. Они перегружены и приводят к ошибкам!
статья [8]про этот инструмент. Тем не менее, добавлю несколько замечаний от себя в сравнении с 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,
})
}
Исходя из вышеперечисленного, я предпочел swagger-typescript-api.
В package.json добавим и запустим команду ( все флаги простые и лаконично описаны на первой странице документации [9] ).
"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 [10] добавим base url и подключим к переменным окружения.
export default defineNuxtConfig({
// где-то тут ваши остальные настройки
runtimeConfig: {
public: {
BACKEND_URL: process.env.BACKEND_URL,
},
},
Создадим плагин [11]для того, чтобы иметь удобный и глобальный способ импортировать наши 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(),
},
},
}
})
Идём в компонент и используем плагин. Не забывайте и не игнорируйте специальные композиции [12] 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 [13].
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 ) .
Так что не забываем обговаривать ваше решение с коллегами :-)
Автор: Evtera
Источник [14]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/api/394186
Ссылки в тексте:
[1] VUE Mastery: https://www.vuemastery.com/blog/api-management-in-nuxt-3-with-typescript/#creating-a-nuxt-plugin
[2] небольшой вводный видео-гайд: https://www.youtube.com/watch?v=jXH8Tr-exhI
[3] medium: https://medium.com/@luizzappa/nuxt-3-repository-pattern-organising-and-managing-your-calls-to-apis-with-typescript-acd563a4e046
[4] кодогенерации: https://www.npmjs.com/package/openapi-typescript
[5] fetch-client: https://www.npmjs.com/package/openapi-fetch
[6] следующим образом: https://github.com/openapi-ts/openapi-typescript/blob/main/packages/openapi-fetch/examples/vue-3/src/composables/catfact-query.ts
[7] советом из документации: https://openapi-ts.dev/examples
[8] статья : https://habr.com/ru/articles/799853/
[9] первой странице документации: https://www.npmjs.com/package/swagger-typescript-api
[10] runtimeConfig : https://nuxt.com/docs/api/nuxt-config#runtimeconfig-1
[11] плагин : https://nuxt.com/docs/guide/directory-structure/plugins
[12] специальные композиции: https://nuxt.com/docs/getting-started/data-fetching
[13] interceptors: https://axios-http.com/docs/interceptors
[14] Источник: https://habr.com/ru/articles/837584/?utm_source=habrahabr&utm_medium=rss&utm_campaign=837584
Нажмите здесь для печати.