How to organize work with API in Nuxt 3 without noise and dust
And what do we have now?
Having asked the question“how to optimally organize work with API in nuxt 3?”, I was faced with the harsh reality: there are not many scalable solutions, and everyone talks about the Repository Pattern
In my opinion, this approach has an obvious downside – many routine work with typing and creating the methods themselves, packaging them, and keeping them up to date.
OpenApi code generation is here to help. Let's take a quick look at the two tools: openapi-typescript And swagger-typescript-api
Openapi-typescript
The project consists of two parts: code generation And fetch-clientwhich is optional, but makes it fairly easy to use generated types.
For example, in the context of VUE fetch-client is used as follows. And here we can note that this tool is ideal for the same Repository Pattern. At least, we do not need to write types ourselves.
The undeniable advantage of this tool for me is the maximum typing of http responses for all possible codes (and not just 200 ).
If in OpenAPI There is 404 And 500then we can easily get them and use them in the future (goodbye empty alerts due to non-standard and varied responses from the back)
The types will look like this:
type ErrorResponse500 =
paths["/my/endpoint"]["get"]["responses"][500]["content"]["application/json"]["schema"];
type ErrorResponse404 =
paths["/my/endpoint"]["get"]["responses"][404]["content"]["application/json"]["schema"];
I know you're thinking how great it would be to make a generic. But I'm sorry to disappoint you. advice from the documentation:
A good fetch-wrapper should never use generics. They are overloaded and error-prone!
swagger-typescript-api
This package has several key differences. And there is a detailed article about this tool on Habr. However, I will add a few comments from myself in comparison with openapi-typescript.
Plus: It has deeper customization (just look at the number of optional parameters) and allows you to flexibly create code generation templates yourself.
Also, which is quite important for me, you can choose axios as an http client (I don't need to use native fetch, and I don't want to reinvent the wheel to track upload/download progress, work with interceptors, timeouts, resets, signals, etc., because it already exists verified and a reliable solution)Plus: All methods are already wrapped in one big class, which allows you to conveniently manipulate them (OOP – your time has come)
Minus: With minimal settings it won't be possible to type errors as coolly as with the previous solution, but remember that everything is possible with templates.
As a result, during code generation we will get:
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, }) }
Connecting to Nuxt 3
Based on the above, I preferred swagger-typescript-api.
IN package.json let's add and run the command (all flags are simple and briefly described on first page of documentation ).
"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",
}
IN nuxt.config.ts runtimeconfig let's add base url and connect to the environment variables.
export default defineNuxtConfig({
// где-то тут ваши остальные настройки
runtimeConfig: {
public: {
BACKEND_URL: process.env.BACKEND_URL,
},
},
Let's create plugin in order to have a convenient and global way to import our API-methods, and also get access to the instance Nuxt and his 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(),
},
},
}
})
Let's go to the component and use the plugin. Don't forget and don't ignore special compositions nuxt when working with API. This is extremely important, especially when working with 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>
Great! We got it all. The API is typed, and the IDE gives us handy hints, while we retained all the Nuxt features.
BUT What about error handling? Let's check!
// такого сервиса не существует
const { $apiService } = useNuxtApp()
const { data, status, error, execute } = useAsyncData('services/list', () =>
$apiService.v1.servicesListRetrieve({ service_id: 111111111111111111 }),
)
IN error we get:
{
"message": "Request failed with status code 404",
"statusCode": 500
}
Hmm… Confusion with codes.
statusCode equal 500 although in fact it is equal 404
Request URL: http://127.0.0.1:8000/api/v1/services/list/?service_id=11111111
Request Method: GET
Status Code: 404 Not Found
Besides, in response there is an error message, however, we do not see it in error at useAsyncData
{
"detail": "Сервиса с таким айди не существует"
}
What's the solution? It's quite simple, we just need to handle the axios promise properly. And to our aid come 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(),
},
},
}
})
and in error we will already receive:
{
"message": "",
"statusCode": 404,
"statusMessage": "Not Found",
"data": {
"detail": "Сервиса с таким айди не существует"
}
}
Summary
This method allows you to use typed methods with minimal effort. API in conjunction with Nuxt.
In general, it will work like this:
We are installing npx command in the pre-commit hook, and if possible, create a job in our pipeline;
Let's start the check typescript throughout the project;
We see typescript errorscancel the commit/drop the job and go fix it. Among other things, when comparing versions in git we will see what exactly has changed, and you won’t have to constantly run to the backend developers for this information.
It is also worth understanding that code generation relies entirely on your OpenAPIwhich is the responsibility of backend developers. And if they ignore the specification for some reason and generally do not pay due attention to it, then you will shoot yourself in the foot with such a tool ( hello // @ts-ignore ) .
So don't forget to discuss your decision with your colleagues 🙂