Skip to main content
Servix Iran market data API
Documentation menu

Vue and Pinia API guide

Use the Servix API with Vue 3, Pinia, and the Composition API

Build and test a real Vue 3, TypeScript, Pinia, and Composition API integration for selected Servix market data with a secure server-side API-key proxy.

This is a working Servix integration, not a generic Vue tutorial with renamed entities. The checked project under docs/examples/vue-pinia uses the real GET /api/v1/assets?codes=... contract, X-API-Key authentication, Servix field names, stable error behavior, bounded retries, and a shared backend cache. Its frontend and server tests run from a clean checkout.

The browser never calls the authenticated Servix endpoint. Vue talks only to the same-origin /api/quotes route; the Node proxy reads SERVIX_API_KEY from its server environment, allowlists three selected assets, and maps the authenticated response to the fields this application needs.

Run the complete project first

Use Node.js 22 or newer. Clone the repository, then execute the exact commands used to verify this guide:

cd docs/examples/vue-pinia
npm ci
cp .env.example .env
# Set SERVIX_API_KEY only in .env on your server.
npm test
npm run build
npm run dev

Open http://127.0.0.1:5175. The launcher starts Vite and the same-origin API proxy together. The committed lockfile makes the install reproducible; the test suite covers the typed client, Pinia state transitions, allowlisting, authentication header, response validation, cache, 429 handling, selected 5xx retries, security headers, and safe errors.

Understand the security boundary

LayerResponsibilityMust not contain
Vue componentRender selected quotes and accessible loading, empty, and error states.API Key or unrestricted Servix responses.
Pinia storeOwn selected codes, request lifecycle, validated quotes, and safe request IDs.Server environment or credential state.
Same-origin clientCall only /api/quotes?codes=... and validate the application DTO.A direct URL to the authenticated customer API.
Node proxyAllowlist codes, add X-API-Key, validate Servix fields, cache, and map safe failures.Credentials in URLs, bodies, logs, or client responses.

Never prefix this secret with VITE_. Vite deliberately copies variables with that prefix into browser-accessible code. Use an application secret manager in production and inject SERVIX_API_KEY only into the Node process.

Install Pinia in the Vue entry point

The runnable src/main.ts creates one Pinia instance before mounting the application:

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

Use a typed same-origin client

src/api/marketQuotes.ts serializes only the selected allowlisted codes, keeps credentials on the same origin, maps stable problem fields, and rejects malformed successful responses:

export async function fetchMarketQuotes(
  codes: readonly AssetCode[],
  signal?: AbortSignal,
): Promise<MarketQuoteResponse> {
  const query = new URLSearchParams({ codes: codes.join(',') })
  const response = await fetch('/api/quotes?' + query, {
    headers: { Accept: 'application/json' },
    credentials: 'same-origin',
    signal,
  })

  const payload: unknown = await response.json().catch(() => null)
  if (!response.ok) {
    const problem = readProblem(payload)
    throw new MarketQuotesError(
      problem.message ?? 'Servix data is temporarily unavailable.',
      problem.code ?? 'REQUEST_FAILED',
      response.status,
      problem.requestId,
    )
  }

  return parseQuoteResponse(payload)
}

The complete file validates code, label, quoteUnit, a finite value, and timezone-aware businessTime. It does not trust JSON merely because the status is 200.

Model request state with a setup-style Pinia store

The store is written with Composition API primitives. A refresh aborts the previous request, an empty selection avoids an unnecessary call, and only a safe message and request ID reach the error state:

export const useMarketQuotesStore = defineStore('marketQuotes', () => {
  const selectedCodes = ref<AssetCode[]>(['USD_RLS', 'EUR_RLS'])
  const quotes = ref<MarketQuote[]>([])
  const status = ref<QuoteLoadStatus>('idle')
  const errorMessage = ref<string | null>(null)
  const requestId = ref<string | null>(null)
  const loadedAt = ref<string | null>(null)
  let activeRequest: AbortController | null = null

  const isLoading = computed(() => status.value === 'loading')
  const hasSelection = computed(() => selectedCodes.value.length > 0)

  function toggleCode(code: AssetCode): void {
    selectedCodes.value = selectedCodes.value.includes(code)
      ? selectedCodes.value.filter((selected) => selected !== code)
      : ASSET_CODES.filter((candidate) => [...selectedCodes.value, code].includes(candidate))
  }

  async function load(): Promise<void> {
    activeRequest?.abort()
    if (!hasSelection.value) {
      quotes.value = []
      status.value = 'empty'
      return
    }

    const request = new AbortController()
    activeRequest = request
    status.value = 'loading'
    errorMessage.value = null
    requestId.value = null

    try {
      const response = await fetchMarketQuotes(selectedCodes.value, request.signal)
      quotes.value = response.quotes
      loadedAt.value = new Date().toISOString()
      status.value = response.quotes.length > 0 ? 'ready' : 'empty'
    } catch (error) {
      if (error instanceof DOMException && error.name === 'AbortError') return
      quotes.value = []
      status.value = 'error'
      errorMessage.value =
        error instanceof Error ? error.message : 'Servix data is temporarily unavailable.'
      requestId.value = error instanceof MarketQuotesError ? (error.requestId ?? null) : null
    } finally {
      if (activeRequest === request) activeRequest = null
    }
  }

  function cancel(): void {
    activeRequest?.abort()
    activeRequest = null
  }

  return {
    selectedCodes,
    quotes,
    status,
    errorMessage,
    requestId,
    loadedAt,
    isLoading,
    hasSelection,
    toggleCode,
    load,
    cancel,
  }
})

Connect lifecycle behavior through a composable

The composable keeps component setup small, preserves Pinia reactivity with storeToRefs, loads on mount, and cancels work when its scope is disposed:

export function useMarketQuotes() {
  const store = useMarketQuotesStore()
  const state = storeToRefs(store)

  onMounted(() => void store.load())
  onScopeDispose(() => store.cancel())

  return {
    ...state,
    toggleCode: store.toggleCode,
    refresh: store.load,
  }
}

App.vue consumes this composable and renders checkboxes, a manual refresh button, loading and error announcements, quote cards, explicit units, and semantic time elements. The UI never substitutes zero for missing data.

Call the real Servix contract from the backend

The checked Node proxy builds the selected-price URL, sends the API Key only as a header, enforces a timeout, retries only network and selected 5xx failures, stops immediately on quota exhaustion, and validates the response before caching it:

async function requestQuotes(codes) {
    const url = new URL('/api/v1/assets', normalizedBaseUrl)
    url.searchParams.set('codes', codes.join(','))

    for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
      let response
      try {
        response = await fetchImpl(url, {
          headers: {
            Accept: 'application/json',
            'X-API-Key': normalizedApiKey,
          },
          redirect: 'error',
          signal: AbortSignal.timeout(timeoutMs),
        })
      } catch (error) {
        if (attempt === maximumAttempts) {
          throw new ServixProxyError('Servix could not be reached.', {
            code: 'SERVIX_NETWORK_ERROR',
            status: 503,
            cause: error,
          })
        }
        await sleep(retryDelay(attempt))
        continue
      }

      const requestId = response.headers.get('x-request-id')
      if (response.ok) {
        const payload = await response.json().catch(() => null)
        return validateQuotes(payload, codes, requestId)
      }

      if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < maximumAttempts) {
        await sleep(retryDelay(attempt))
        continue
      }
      throw mapUpstreamError(response.status, requestId)
    }

    throw new ServixProxyError('Servix could not be reached.')
  }

The full server rejects credentials containing whitespace, requires HTTPS except for loopback contract tests, permits only USD_RLS, EUR_RLS, and GOLD_18_RLS, preserves request order, and returns Cache-Control: no-store to browsers. Its successful in-memory cache is shared by requests to that Node process for 30 seconds by default.

Use the authenticated response accurately

The public tutorial uses placeholders rather than publishing a market observation:

{
  "code": "USD_RLS",
  "label": "US dollar / Iranian rial",
  "quoteUnit": "RLS",
  "value": "<protected-market-value>",
  "businessTime": "<source-timestamp>"
}
  • quoteUnit is part of the contract; display it instead of guessing rial, dollar, or index units from a label.
  • businessTime is the market observation time, not the browser refresh time.
  • 429 means the daily quota is exhausted. Stop automatic refreshes; do not create a retry loop.
  • Keep the proxy allowlist aligned with the small set your product actually displays. Do not turn it into an unrestricted API mirror.

Prove the integration instead of trusting snippets

npm test runs deterministic synthetic contract tests without a real credential. After configuring a valid server-side test key, run the separate authenticated smoke check:

npm run smoke:real

The smoke command starts this same proxy on an ephemeral loopback port and reaches Servix through /api/quotes. It validates that the protected field is numeric but prints only the asset code, unit, business time, and a boolean result—never the API Key or market value.

Deploy with the same boundary

  • Run npm ci, npm test, and npm run build in CI.
  • Inject SERVIX_API_KEY into the Node service from a server-side secret manager.
  • Keep the browser and proxy on the same HTTPS origin, or add a deliberately narrow CORS policy if your architecture requires separate origins.
  • Use a shared external cache when multiple Node instances must share quota-efficient results.
  • Monitor safe error codes and request IDs; never record credentials, response bodies, or market values in client-visible logs.

Continue with API-key authentication, current-price endpoints, supported assets, and errors and quotas. Compare the Python, Spring Boot, and PHP implementations.

Create an account and activate API access · Compare plans and daily request limits