Skip to main content

API integration overview

This section explains how the package communicates with the Propeller GraphQL backend — the layers a request passes through, who owns what, and where to look when something goes wrong. The per-domain pages (cart, auth, catalog, orders, B2B) then go composable-by-composable.

For the exact signature of any symbol named here, see the generated API reference.

The request path

Every backend call made by the package travels the same path:

component / your code
│ calls

composable (use* function) ← reactive state, loading, error handling
│ calls

Services bundle (services.x) ← built once by createServices()
│ delegates to

propeller-sdk-v2 Service class ← builds the GraphQL document
│ uses

GraphQLClient ← YOUR endpoint, headers, auth
│ HTTP

Propeller GraphQL API

The package owns the middle three layers. It does not own the GraphQLClient — you construct that — and it does not own the network transport. See The SDK seam for why.

GraphQLClient — you build it

The package ships no client and no endpoint. You construct a GraphQLClient from propeller-sdk-v2 with the endpoint, headers, timeout and auth resolver that fit your app, and you keep ownership of it.

import { GraphQLClient } from 'propeller-sdk-v2';

const graphqlClient = new GraphQLClient({
endpoint: '/api/graphql', // your route / proxy / upstream URL
apiKey: '',
timeout: 30_000,
});

The client is stateful: SDK code mutates its config in place (graphqlClient.updateConfig({ headers })) when auth changes — see Authentication. Everything downstream reads the current headers at request time, so a login that updates the client is immediately visible to every composable.

createServices — the Services bundle

createServices(client) builds one Services object — a typed bundle of 15 SDK service instances (product, cart, user, category, order, payMethod, login, address, company, crossupsell, bundle, favoriteList, purchaseAuthConfig, cluster, orderlist), each wired to the client you passed.

import { createServices } from 'propeller-v2-vue-ui';

const services = createServices(graphqlClient);
// services.cart, services.product, services.order, …

It is memoised per client with a WeakMap — calling it again with the same client returns the same bundle, so there is no cost to calling it freely. You pass services (and graphqlClient) into providePropeller once; from there useServices() returns the bundle anywhere in the tree.

See createServices and Services in the reference.

How composables reach the API

There are two patterns, and the package uses both:

  1. Via the provider — a composable calls useServices() to read the bundle from providePropeller. Used by useServices itself and the pure context composables.
  2. Via an explicit graphqlClient option — most data composables (useCart, useAuth, useOrders, …) accept graphqlClient in their options object and derive a local Services bundle from it with createServices. Because createServices is memoised, this resolves to the same bundle the provider holds.

In practice you pass the same graphqlClient instance everywhere — into the provider and into each composable's options — so both patterns observe one client, one bundle, one auth state.

Reactive options

Unlike the React hooks they mirror, these composables take reactive options. Fields that change over the page's lifetime — user, companyId, language, cartId — are passed as Vue Refs (or computeds), not plain values:

const cart = useCart({
graphqlClient,
user, // Ref<Contact | Customer | null>
companyId, // Ref<number | undefined>
language: ref('NL'),
configuration,
});

The composable unrefs them at call time, so when your auth state or the active company changes, the next action sees the new value with no re-instantiation. There is no Rules-of-Hooks constraint — call a composable conditionally or inside a branch if it makes sense.

toPlain — normalising SDK results

SDK service methods return class instances whose getters forward to underscore-prefixed backing fields (_items, _firstName). Anything that serialises them — JSON, Vue's reactivity diffing, structuredClone — sees the underscored shape, not the clean public type.

toPlain(value) recursively strips that prefix. Apply it once at the service boundary when you need a plain object (for reactive state, for passing to a Nuxt server context, for logging). The package's own components handle this internally; you only need toPlain for data you fetch yourself.

Error handling convention

API-talking composables do not throw to the caller. Each async action returns a result object — typically { success: boolean; error?: string }, sometimes with a payload (cart, orderId, user). Loading and the last error are also surfaced as loading / error refs on the composable return.

const { addItem, loading, error } = useCart({ graphqlClient, user, configuration });

const result = await addItem({ product, quantity: 1 });
if (!result.success) {
// result.error is a human-readable message
}

The exception is useServices(), which throws if called outside providePropeller — that is an integration mistake, not a runtime error, so it fails loudly and early.

Authentication, in one paragraph

useAuth().login() registers the session token on the GraphQLClient you own (setAccessToken) for the rest of the page session, and hands it back to you through onAuthHeaderUpdate and the result object. Cross-reload auth is your app's responsibility — typically an httpOnly cookie set by your backend, with your GraphQL proxy injecting the Bearer header server-side. Full detail on the auth page.