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.

Somehow I don't feel like it...

Somehow I don't feel like it…

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)

answer options

answer options

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.

  1. 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)

  2. Plus: All methods are already wrapped in one big class, which allows you to conveniently manipulate them (OOP – your time has come)

  3. 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
response in browser

response in browser

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:

  1. We are installing npx command in the pre-commit hook, and if possible, create a job in our pipeline;

  2. Let's start the check typescript throughout the project;

  3. 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 🙂

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *