@qualisero/openapi-endpoint

Type-safe OpenAPI integration for Vue Query
6,354
Install
npm install @qualisero/openapi-endpoint
Latest Version:0.28.2
Vue:3
License:MIT
Last Updated:Oct 7, 2026
Links: GitHub  ·  npm
Maintainer: davedv

OpenApiEndpoint

npm version CI License: MIT Documentation

Type-safe API composables for Vue using TanStack Query. Generate fully-typed API clients from your OpenAPI specification.

Quick Start

// 1. Generate types from your OpenAPI spec
npx @qualisero/openapi-endpoint ./api/openapi.json ./src/generated

// 2. Initialize the API client
import { createApiClient } from '@qualisero/openapi-endpoint'
import axios from 'axios'

const api = createApiClient(axios.create({ baseURL: 'https://api.example.com' }))

// 3. Use in your Vue components
const { data: pets, isLoading } = api.listPets.useQuery()
const { data: pet } = api.getPet.useQuery({ petId: '123' })

const createPet = api.createPet.useMutation()
await createPet.mutateAsync({ data: { name: 'Fluffy', species: 'cat' } })

Features

  • Fully typed - Operations, parameters, and responses type-checked against your OpenAPI spec
  • Reactive parameters - Query params automatically refetch when values change
  • Automatic cache management - Mutations invalidate and update related queries
  • Vue 3 + TanStack Query - Built on proven reactive patterns
  • File uploads - Support for multipart/form-data endpoints

Installation

npm install @qualisero/openapi-endpoint

Code Generation

Basic usage

# From local file
npx @qualisero/openapi-endpoint ./api/openapi.json ./src/generated

# From remote URL
npx @qualisero/openapi-endpoint https://api.example.com/openapi.json ./src/generated

# Also emit JSON Schema value constraints (add 'all' for response schemas too)
npx @qualisero/openapi-endpoint ./api/openapi.json ./src/generated --emit-value-schemas

Config file mode

Create openapi-codegen.config.json in your project root to codegen multiple specs in one invocation:

{
  "options": {
    "enumCase": "const",
    "defaultNonNullable": false
  },
  "specs": [
    {
      "input": "../specs/api-v1.json",
      "output": "src/generated/api-v1"
    },
    {
      "input": "../specs/api-v2.json",
      "output": "src/generated/api-v2"
    }
  ]
}

Run openapi-codegen with no arguments to discover and use the config automatically. Passing positional arguments bypasses the config entirely: one-off runs use CLI flags only.

Common CLI flags

  • --enum-case <pascal|const> — Enum label casing (default: pascal)
  • --emit-value-schemas [request|all] — Emit JSON Schema constraints (default: off)
  • --default-non-nullable <true|false> — Forward to openapi-typescript (default: true). Note: defaultNonNullable: true is only sound for responses; it makes PATCH request bodies non-optional when they carry default: null, which is usually incorrect. The default will flip to false in the next major version.
  • --use-strict-response — Use ApiResponseStrict (only readonly/required fields required) instead of ApiResponse (all fields required)

Generated files:

File Description
api-client.ts createApiClient factory (main entry point)
api-operations.ts Operations map and type helpers
api-types.ts Type namespace for response/request types
api-enums.ts Schema enums
api-schemas.ts Schema type aliases
openapi-types.ts Raw OpenAPI types

With --emit-value-schemas [request|all] (default: off):

File Description
api-value-schemas.ts Standard JSON Schema value constraints (see Value schemas)

API Reference

Initialization

import { createApiClient } from '@qualisero/openapi-endpoint'
import axios from 'axios'

const api = createApiClient(axiosInstance, queryClient?)

Queries (GET/HEAD/OPTIONS)

// No parameters
const { data, isLoading, error, refetch } = api.listPets.useQuery()

// With path parameters
const { data } = api.getPet.useQuery({ petId: '123' })

// With query parameters
const { data } = api.listPets.useQuery({
  queryParams: { limit: 10, status: 'available' },
})

// Reactive parameters
const limit = ref(10)
const { data } = api.listPets.useQuery({
  queryParams: computed(() => ({ limit: limit.value })),
})
// Automatically refetches when limit.value changes

// With options
const { data, onLoad } = api.listPets.useQuery({
  enabled: computed(() => isLoggedIn.value),
  staleTime: 5000,
  onLoad: (data) => console.log('Loaded:', data),
})

// onLoad callback method
const query = api.getPet.useQuery({ petId: '123' })
query.onLoad((pet) => console.log('Pet:', pet.name))

Request cancellation: TanStack's AbortSignal is forwarded to axios, so a cancelled query (queryClient.cancelQueries, last observer unmounting mid-fetch, or a mutation on the same key) aborts the network request instead of only discarding the result. To drive cancellation yourself, pass axiosOptions: { signal }: an explicit signal takes priority over the TanStack one (call-time fetch() options win over hook-time options for useLazyQuery).

Getting a URL without fetching

Every query operation also exposes urlFor(), which returns a plain URL string without performing a request. This is the right tool for <img :src>, anchor href, window.open, or native fetch:

const url = api.getAssetDocumentFile.urlFor({ asset_id, document_ref }, { view: true })
// → "https://api.example.com/api/v3/document/asset/.../file?view=true"

Notes:

  • Synchronous; takes plain values only (not refs or getters). Wrap in computed(...) for reactivity.
  • Only flat scalar query params (string | number | boolean) are supported.
  • baseURL is read from the axios instance at call time.
  • Available on query operations only (GET / HEAD / OPTIONS), not mutations.

Mutations (POST/PUT/PATCH/DELETE)

// Simple mutation
const createPet = api.createPet.useMutation()
await createPet.mutateAsync({ data: { name: 'Fluffy' } })

// With path parameters
const updatePet = api.updatePet.useMutation({ petId: '123' })
await updatePet.mutateAsync({ data: { name: 'Updated' } })

// Deferred path params: omit at hook time, provide at call time
const deletePet = api.deletePet.useMutation()
await deletePet.mutateAsync({ pathParams: { petId: '123' } })

// Deferred path params with options
const updateWithCache = api.updatePet.useMutation(undefined, {
  invalidateOperations: { listPets: {} },
})
await updateWithCache.mutateAsync({
  data: { name: 'Updated' },
  pathParams: { petId: '123' },
})

// With options
const mutation = api.createPet.useMutation({
  dontInvalidate: true,
  invalidateOperations: ['listPets'],
  onSuccess: (response) => console.log('Created:', response.data),
})

Cache guarantee for PUT/PATCH: when the response body is written to the cache (the default unless dontUpdateCache: true or the body is empty), any in-flight GET on the same key is cancelled first. After mutateAsync resolves, useQuery(...).data for that key equals the response body, and the item query is left fresh (not invalidated, no refetch spawned). The list path is still invalidated.

Serialised mutations

The serialize option queues mutations so they run one at a time in submission order, preventing last-write-wins races in auto-save flows. It maps directly to TanStack Query v5's scope mechanism.

// Auto-scope: derived from the resolved path — same resource, same queue
const patch = api.updateContract.useMutation({ contract_id: '123' }, { serialize: true })
await patch.mutateAsync({ data: changes1 })
await patch.mutateAsync({ data: changes2 }) // waits for changes1 to complete first

// String scope: share a queue across different operations
const updatePet = api.updatePet.useMutation({ petId: '1' }, { serialize: 'pet-editor' })
const updateStatus = api.updatePetStatus.useMutation({ petId: '1' }, { serialize: 'pet-editor' })
// Both mutations queue behind each other in submission order

// Raw TanStack scope passthrough also works without serialize:
const mutation = api.updatePet.useMutation({ petId: '1' }, { scope: { id: 'my-scope' } })

serialize: true scope derivation: the scope id is serialize:<METHOD>:<resolvedPath>, e.g. serialize:PATCH:/api/contract/123. Two components mutating the same resource (same path params at hook time) share a queue automatically; different resources do not block each other. The scope id is reactive: if hook-time path params are refs/getters and change, subsequent mutations use the updated scope.

Deferred path params caveat: when path parameters are supplied at mutateAsync time rather than hook time, the scope id falls back to the path template (e.g. serialize:PATCH:/api/contract/{contract_id}), serialising all mutations of that operation regardless of target resource. For per-resource granularity, supply path parameters at hook-creation time or use a string scope.

Explicit scope always wins: if both scope and serialize are set, scope takes precedence and a warning is logged.

Global error policy

createApiErrorCaches builds the queryCache and mutationCache for your QueryClient with a single onError callback that observes every failure from hooks created by this library. It fires once per logical failure, after retries, and never swallows the rejection — the right place for a global error banner:

import { createApiClient, createApiErrorCaches } from '@qualisero/openapi-endpoint'
import { QueryClient } from '@tanstack/vue-query'
import axios from 'axios'

const { queryCache, mutationCache } = createApiErrorCaches({
  onError: (error, ctx) => {
    // ctx: { operationId, path, method, kind: 'query' | 'mutation' }
    showErrorBanner(`Request to ${ctx.operationId} failed`)
  },
})

const queryClient = new QueryClient({ queryCache, mutationCache })
const api = createApiClient(axios.create({ baseURL: 'https://api.example.com' }), queryClient)

Notes:

  • The callback is observe-only: the query or mutation still rejects.
  • It fires for any rejection, including non-axios throws — narrow with isAxiosError (from axios) inside the callback.
  • Queries and mutations you register yourself (outside the generated hooks) are untouched.

Opt out per call with skipGlobalError — true always suppresses the global policy for that call, or a predicate typed to the operation's declared error body suppresses it selectively:

// Never trigger the global banner for this query
const { data } = api.getPet.useQuery({ petId: '123' }, { skipGlobalError: true })

// Skip only specific errors (e.g. you handle 409 inline)
const createPet = api.createPet.useMutation({
  skipGlobalError: (e) => e.response?.status === 409,
})

The promise rejects either way — skipGlobalError controls the global side-effect only.

Typed errors

error.value is AxiosError<TError> | null, where TError is the operation's declared error body (the union of JSON bodies from its 4xx / 5xx / default responses). No more casting:

const { error, mutateAsync } = api.createPet.useMutation()

// after a rejection, the declared error body is fully typed:
error.value?.response?.data?.errors // no cast, no hand-written interface

The generated api-operations.ts also exports helpers for use in plain functions:

import type { ApiError, ApiErrorData } from './generated/api-operations'

type PetErrorBody = ApiErrorData<'createPet'>
function isConflict(e: ApiError<'createPet'>): boolean {
  return e.response?.status === 409
}

Operations that declare no JSON error body fall back to unknown.

Axios Configuration

Pass custom Axios options through the axiosOptions parameter for advanced HTTP configuration:

// Custom headers
const { data } = api.listPets.useQuery({
  axiosOptions: {
    headers: {
      'X-Custom-Header': 'custom-value',
      Authorization: 'Bearer token123',
    },
  },
})

// Timeout configuration
const { data } = api.getPet.useQuery(
  { petId: '123' },
  {
    axiosOptions: {
      timeout: 5000, // 5 second timeout
    },
  },
)

// Request/response transforms
const { data } = api.createPet.useMutation({
  axiosOptions: {
    transformRequest: [(data) => JSON.stringify(data)],
    transformResponse: [(data) => JSON.parse(data)],
  },
})

// Override baseURL for specific requests
const { data } = api.listPets.useQuery({
  axiosOptions: {
    baseURL: 'https://custom-api.example.com',
  },
})

// Custom properties (via AxiosRequestConfigExtended)
const { data } = api.listPets.useQuery({
  axiosOptions: {
    timeout: 5000,
    manualErrorHandling: true, // Custom property
    handledByAxios: false, // Custom property
    customRetryCount: 3, // Custom property
  },
})

Common Axios options supported:

  • headers - Custom request headers
  • timeout - Request timeout in milliseconds
  • baseURL - Override base URL for this request
  • auth - Basic authentication
  • withCredentials - Include cookies in cross-origin requests
  • params - URL parameters (merged with queryParams)
  • paramsSerializer - Custom parameter serializer
  • transformRequest / transformResponse - Request/response transformers
  • onUploadProgress / onDownloadProgress - Progress callbacks
  • signal - Request cancellation with AbortController
  • validateStatus - Custom status code validation

Note: The AxiosRequestConfigExtended type also supports arbitrary custom properties beyond standard Axios options.

Return Types

Query Return:

{
  data: ComputedRef<T | undefined>
  isLoading: ComputedRef<boolean>
  error: Ref<AxiosError<TError> | null>
  isEnabled: ComputedRef<boolean>
  queryKey: ComputedRef<unknown[]>
  onLoad: (cb: (data: T) => void) => void
  refetch: () => Promise<void>
}

Mutation Return:

{
  data: ComputedRef<AxiosResponse<T> | undefined>
  isPending: ComputedRef<boolean>
  error: Ref<AxiosError<TError> | null>
  mutate: (vars) => void
  mutateAsync: (vars) => Promise<AxiosResponse>
  extraPathParams: Ref<PathParams> // for dynamic path params
}

Type Helpers

Response and request types
import type {
  ApiResponse, // Response type (ALL fields required - default)
  ApiResponseStrict, // Response type (only readonly/required fields required - strict mode)
  ApiRequest, // Request body type
  ApiPathParams, // Path parameters type
  ApiQueryParams, // Query parameters type
} from './generated/api-operations'

// ApiResponse - default, ALL fields required
type PetResponse = ApiResponse<OpType.getPet>
// { readonly id: string, name: string, tag: string, status: 'available' | ... }

// ApiResponseStrict - strict mode, only readonly/required fields required
type PetResponseStrict = ApiResponseStrict<OpType.getPet>
// { readonly id: string, name: string, tag?: string, status?: 'available' | ... }

Transitional note — Response vs StrictResponse duality: ApiResponse (the default) wraps the spec type in RequireAll<T>, asserting that the API serialises every documented field. This is a stopgap for specs that do not yet encode dump-direction required explicitly — it may be overly strict if the backend omits optional fields. ApiResponseStrict is spec-faithful (only required/readonly fields are required).

Specs stamped with x-direction-finalized: true (an OAS extension set by the BE finalize pass) already encode presence policy as dump-direction required. For those specs Response ≡ StrictResponse and --use-strict-response is redundant — the codegen will log an advisory. In a later major release the duality collapses and the RequireAll wrapper is dropped for finalized specs.

Direction utilities

Three utilities are exported for working with response/request direction transformations:

import type { RequireAll, Writable, Mutable } from '@qualisero/openapi-endpoint'

// RequireAll<T> — deep-require all fields (the policy applied by ApiResponse)
type StrictPet = RequireAll<Pet>

// Writable<T> — convert response → request (exclude readonly fields, recurse nested objects/arrays)
type PetRequest = Writable<Pet>

// Mutable<T> — strip readonly modifiers without removing keys (useful for store/copy shapes)
type MutablePet = Mutable<Pet>
Response-reachable schemas namespace

The generated api-schemas.ts exports a Responses namespace containing direction-typed aliases for all response-reachable components, with traceability JSDoc:

import type { Responses } from './generated/api-schemas'

// Response-reachable schemas are wrapped in RequireAll (same policy as ApiResponse)
// JSDoc shows which operations reference each schema
type AssetDetail = Responses.AssetDetail

Bare component aliases (e.g. export type Asset = components['schemas']['Asset']) remain for one minor cycle but are deprecated — migrate to Responses.* for response-typed access or Types.<opId>.Request for request bodies.

Enums

The CLI generates type-safe enum constants:

import { PetStatus } from './generated/api-enums'

// Use enum for intellisense and typo safety
const { data } = api.listPets.useQuery({
  queryParams: { status: PetStatus.Available },
})

// Still works with string literals
const { data } = api.listPets.useQuery({
  queryParams: { status: 'available' }, // also valid
})
ALL_CAPS enum labels

Pass --enum-case const to generate SCREAMING_SNAKE_CASE member labels instead of the default PascalCase:

npx openapi-endpoint --input openapi.json --output src/generated --enum-case const
import { PetStatus } from './generated/api-enums'

// Labels are ALL_CAPS; values sent over the wire are unchanged
const { data } = api.listPets.useQuery({
  queryParams: { status: PetStatus.AVAILABLE }, // value is still 'available'
})

Only code-level labels change. The values sent over the wire, the emitted union types, and the runtime library behaviour are all unchanged.

Value schemas

Pass --emit-value-schemas to additionally generate api-value-schemas.ts: per-operation request (and, with all, response) value constraints as standard JSON Schema data, with shared component schemas in a schemaDefs map. Use resolveSchema to compose a self-contained schema document for any JSON-Schema tool — validators (AJV 8 with ajv-formats registered, or validateFormats:false, because format keywords are emitted by design), form generators (JSONForms, react-jsonschema-form, vjsf), etc.:

npx @qualisero/openapi-endpoint ./api/openapi.json ./src/generated --emit-value-schemas all

Install the validator in your app (ajv and ajv-formats are not transitive dependencies of this package): npm i ajv ajv-formats. Register ajv-formats because emitted schemas carry format keywords (date-time, uuid, …) and AJV 8 throws on unknown formats in its default strict mode; alternatively pass new Ajv({ validateFormats: false }) to skip format checking entirely.

import Ajv from 'ajv'
import addFormats from 'ajv-formats'
import { resolveSchema } from '@qualisero/openapi-endpoint'
import { requestSchemas, schemaDefs } from './generated/api-value-schemas'

const ajv = addFormats(new Ajv())
const validate = ajv.compile(resolveSchema(requestSchemas.createPet, schemaDefs))
validate({ name: 'Fluffy', species: 'cat' }) // true or false, errors in validate.errors

Request bodies and responses referenced via #/components/requestBodies/... / #/components/responses/... are dereferenced one level, and any JSON media type is accepted: exact application/json first, then application/*+json variants such as application/vnd.api+json (parameters like ; charset=utf-8 are allowed). Operations with no extractable JSON schema (e.g. text/plain only) are skipped with a codegen warning.

For hand-rolled form rules or tables, fieldsOf(requestSchemas.createPet, schemaDefs) returns a flat list of fields with their constraints (type, required, enum, minLength, maxLength, minimum, maximum, format, readOnly, nullable).

Documentation

For detailed guides, see docs/manual/:

License

MIT

Related Packages

openapi-vue-query

Fast, type-safe @tanstack/vue-query client to work with your OpenAPI schema.

4,173 6
@comet-cli/plugin-documentation

Plugin for generating API documentation

6,405 0
vitepress-openapi-docs

Interactive OpenAPI documentation for VitePress. Lightweight, Vue-native, compos...

3,376 6
@ts-rest/vue-query

vue-query client integration for @ts-rest

28,810 3,343
vue-restify

rest API requests

1,112 0

Version History

Version Released Vue License
0.28.2 >=3.3 MIT
0.28.1 >=3.3 MIT
0.28.0 >=3.3 MIT
0.27.1 >=3.3 MIT
0.27.0 >=3.3 MIT