Request class
The API class resolves endpoints by url_name from Application Load, sends request data and returns the HTTP response together with the API result.
Endpoint contracts are shared across technologies. The class documented here is the Nuxt transport also used by the API playground; it depends on the Nuxt application context, $fetch and cookies.
Endpoint discovery
After a successful POST /api/appload, public endpoints are in page_data.unauth.paths; the current authenticated session can also receive page_data.auth.paths. These objects group endpoint records by category and route. Look up the record by its url_name.
- 1
POST /api/apploadReceive the endpoint metadata for the current session. - 2
url_nameFind the endpoint by its stable name. - 3
target + methods[0]Use the discovered URL and first declared HTTP method.
| Property | Purpose |
|---|---|
IAppLoadPathAn endpoint record within the public or authenticated paths returned by Application Load. | |
url_name | Stable endpoint identifier used by API.fromName(). It is not the request URL. |
target | Backend-provided route. The named factory normalizes its leading slash. |
methods | Declared HTTP methods. The named factory takes methods[0]; it does not infer POST from an endpoint name. |
forms | Available form/filter metadata. Body nesting and accepted fields must still follow the endpoint contract; discovery metadata is not a complete request schema. |
API.fromName() searches authenticated paths first, then public paths. If the name is absent, or its target or first method is missing, it returns undefined. A documentation catalog entry does not make an endpoint available. The backend still checks permissions when the request arrives.
Creating a request
Creating an API instance does not send a request. API.fromName<T>(url_name) selects both the URL and HTTP method from the current Application Load response. Use the named lookup for discovered endpoints.
| Property | Purpose |
|---|---|
API.fromName<T>(url_name) | Returns API<T> for a current endpoint or undefined when unavailable. Uses the discovered target in exact-URL mode. |
new API<T>(url, method) | Creates a request from an explicit URL and optional HTTP method. Without a method, fetch() uses POST. This constructor does not perform Application Load discovery. |
useAPI<T>(url, method) | Returns a new API<T> instance for an explicit URL and optional method, with the same behavior as the constructor. |
Each instance captures the resolved URL and method when it is created. After Application Load changes, create a new instance to use the current discovery data. An existing instance does not update itself or refresh the session.
Request data and URL
Configuration methods return the same instance. fetch() sends the request body in the format documented for the endpoint.
| Property | Purpose |
|---|---|
setBody(body) | Stores an object or FormData for a later fetch. The object is passed as the request body without adding endpoint-specific nesting. |
fetch(body, options) | Sends the request. Its body argument takes precedence over the stored body; omitting it uses the value from setBody(). Transport options accept signal, timeout and retry. |
setHeaders(headers) | Replaces the configured custom header collection. The transport then sets its CSRF, origin, referer and time-zone headers; these are not overridden by the custom collection. |
setPathParam(name, value) | Replaces the first matching named path placeholder in the current URL with the supplied string or number. Values are inserted as supplied, without URL encoding. |
setPathParams(params) | Applies the supplied string values one by one through setPathParam(). |
URL and language settings
| Property | Purpose |
|---|---|
getURL() | Returns the initial URL, before setURL or path parameter changes. |
getLastURL() | Returns the last configured URL, or the initial URL when it has not been changed. It does not include language processing performed later by fetch. |
setURL(url) | Stores the URL used by the next fetch. The HTTP method stays unchanged. |
changeURL(fn) | Calls the supplied function with the initial URL and stores its result through setURL(). It does not pass the previously modified URL. |
shouldUseExactURL(should) | Enables or disables URL processing. With true, fetch uses the configured URL unchanged. Named requests enable this mode automatically. |
shouldAddLangToQuery(lang) | In non-exact mode, true appends ?lang= with the current locale; a string supplies an explicit language and false disables this suffix. Calling the method without an argument uses true. |
Named requests use exact-URL mode by default. In this mode, shouldAddLangToQuery() has no effect. The playground prepares its displayed URL with the selected lang parameter before sending it through the same class.
Transport and session
Requests include session cookies. In the browser, the transport reads the CSRF cookie for the request header; during server rendering it also forwards request headers and applies backend Set-Cookie values to the outgoing response.
| Property | Purpose |
|---|---|
credentials | Uses include so requests carry session cookies where the browser permits them. |
X-CSRFToken | Reads the csrftoken cookie for each fetch and sends its value, or an empty string if absent. |
X-TZ | Sends the time zone resolved by the JavaScript runtime. |
options.signal | An AbortSignal supplied by the caller. The class passes it to fetch; cancellation is handled through the failed-request result. |
options.timeout | Timeout in milliseconds passed to $fetch. The class does not set its own timeout when this option is omitted. |
options.retry | Retry setting passed to $fetch. Use 0 to disable automatic retries; omitting it leaves retry behavior to the transport. |
shouldShowLoadingIndicator(should) | Controls the shared browser loading indicator; it is enabled by default. The flag is cleared in finally. The class does not open a sign-in dialog. |
The playground calls API.fromName(url_name) again on Send, sets its own request timeout and disables automatic retries with retry: 0. It blocks duplicate submissions and aborts its request on unmount. It shows its own pending state instead of the global loading indicator.
Response and types
Check HTTP status and the API envelope separately. fetch() returns a result for successful responses and caught transport/HTTP failures. An HTTP 200 response can contain form_errors; the presence of page_data alone does not establish success.
| Property | Purpose |
|---|---|
raw | Raw HTTP response, including status, headers and the decoded body in _data. It may be absent after a network failure. |
page_data | The envelope’s page_data, typed as T. It can be absent or null depending on the endpoint and outcome. |
form_errors | Backend field errors, retained when available. General errors use __all__. Inspect this separately from HTTP status. |
success_message | The envelope’s success_message; null or absent values become undefined. A message alone does not establish transport success. |
error | Convenience error text: the first general or field error, or transport failure text. It does not replace the complete form_errors or raw response. |
T in API<T> describes page_data, not the complete response envelope. It does not validate the request body or response at runtime. Endpoint pages provide separate request and response interfaces. Non-JSON bodies and HTTP details remain available through raw when a response was received.
An HTTP 401 response is returned as the request result; this class does not change the sign-in state of this website. The supplied Nuxt storefront’s shared API client removes the signed-in data and loads the guest session instead, as described in Success and errors.
Success and errorsImplementation source
These code blocks read the maintained implementation files. Copying preserves the actual source; its imported dependencies and Nuxt context are required.
app/functions/api/useAPI.ts
Shared request class, named factory, request configuration and transport response handling.
import { readCookieNow } from '@/functions/api/cookieNow';
import type { NuxtApp } from 'nuxt/app';
import SetCookieParser from 'set-cookie-parser';
import type { IFetch } from '@/interfaces/apiBase/IFetch.ts';
import type { TMethod } from '@/interfaces/apiBase/TMethod.ts';
import { getConditionalURL } from '@/functions/api/url.ts';
import { useCommonStore } from '@/stores/commonStore.ts';
import { findLiveApiEndpoint } from '@/functions/api/findLiveApiEndpoint';
import { useAppLoadStore } from '@/stores/apploadStore';
// Documentation pages read the demo storefront: the same name as DEMO_DATA_HEADER in applicationLoadBoundary.ts.
const DEMO_DATA_HEADER = 'X-Orionplace-Demo-Data';
// Browser requests that show the top loading bar and have not finished yet.
let indicatorRequests = 0;
export class API<T = {}> {
private method?: TMethod;
private initialURL: string;
private lastURL?: string;
private body: Record<string, any> | FormData | null = null;
private headers: Record<string, string | undefined> | undefined = undefined;
private addLangToQuery: boolean | string = true;
private useExactURL = false;
private showLoadingIndicator = true;
constructor(url: string, method?: TMethod | string) {
this.method = method as TMethod;
this.initialURL = url;
}
public static fromName<T = {}>(name: string) {
const endpoint = findLiveApiEndpoint(useAppLoadStore().state, name)?.path;
if (!endpoint?.target || !endpoint.methods[0]) {
return undefined;
}
const url = `/${endpoint.target.replace(/^\/+/, '')}`;
return new API<T>(url, endpoint.methods[0]).shouldUseExactURL();
}
public setBody(body: Record<string, any> | FormData = {}) {
this.body = body;
return this;
}
public setHeaders(
addHeaders: Record<string, string | undefined> | undefined = undefined,
) {
this.headers = addHeaders;
return this;
}
public getURL() {
return this.initialURL;
}
public getLastURL() {
return this.lastURL ?? this.initialURL;
}
public setURL(url: string) {
this.lastURL = url;
return this;
}
public changeURL(fn: (currentURL: string) => string) {
return this.setURL(fn(this.initialURL));
}
public setPathParam(param: string, value: string | number) {
const url = this.getLastURL();
return this.setURL(url.replace('{' + param + '}', String(value)));
}
public setPathParams(params: Record<string, string>) {
Object.entries(params).forEach(([key, value]) =>
this.setPathParam(key, value),
);
return this;
}
public shouldAddLangToQuery(lang: boolean | string = true) {
this.addLangToQuery = lang;
return this;
}
public shouldUseExactURL(should: boolean = true) {
this.useExactURL = should;
return this;
}
public shouldShowLoadingIndicator(should: boolean = true) {
this.showLoadingIndicator = should;
return this;
}
public async fetch<T2 extends T>(
_body?: Record<string, any> | FormData,
options: { signal?: AbortSignal; timeout?: number; retry?: number } = {},
) {
const nuxt = useNuxtApp();
const pinia = nuxt.$pinia as import('pinia').Pinia;
const commonStore = !nuxt.ssrContext ? useCommonStore(pinia) : null;
const headers = this.getDefaultHeaders(nuxt);
const URL = this.useExactURL
? (this.lastURL ?? this.initialURL)
: getConditionalURL(this.lastURL ?? this.initialURL, this.addLangToQuery);
try {
if (commonStore && this.showLoadingIndicator) {
indicatorRequests += 1;
commonStore.isGlobalLoading = true;
}
const result = await $fetch.raw<IFetch<T2>>(URL, {
body: _body ?? this.body,
method: this.method ?? 'POST',
headers: headers,
credentials: 'include',
signal: options.signal,
timeout: options.timeout,
retry: options.retry,
// Forward backend cookies through the SSR response.
onResponse(ctx) {
if (!import.meta.server) {
return;
}
const raw = ctx.response.headers.get('Set-Cookie');
if (!raw) {
return;
}
const cookies = SetCookieParser.parse(
SetCookieParser.splitCookiesString(raw),
);
nuxt.runWithContext(() =>
cookies.forEach((cookie) => {
const dynamic = useCookie(cookie.name, cookie as any);
dynamic.value = cookie.value;
}),
);
},
onResponseError(ctx) {
// The server journal keeps failed backend calls; the browser already lists them in its Network tab.
if (import.meta.server) console.error('[API] Response error', ctx.response?.status, ctx.request);
if (ctx.response?.status === 453) {
if (import.meta.client) {
const message =
ctx.response?._data?.form_errors?.__all__?.[0]?.message ??
'Rate limit error';
localStorage.setItem('453', message);
}
}
},
});
let json: IFetch<T2> | undefined = undefined;
try {
json = result._data;
} catch (e) {
if (import.meta.server) console.error('[API/JSON]', e);
}
return {
raw: result,
page_data: json?.page_data,
form_errors: json?.form_errors,
success_message: json?.success_message ?? undefined,
error: json?.form_errors
? (json.form_errors.__all__?.at(0)?.message ??
Object.values(json.form_errors).at(0)?.at(0)?.message ??
'Error')
: undefined,
};
} catch (ex: any) {
if (import.meta.server) console.error('[API] Request failed', URL, ex?.message ?? ex);
// HTTP failures can still contain the backend response envelope.
const json = ex?.data as IFetch<T2> | undefined;
if (json?.page_data || json?.form_errors) {
return {
raw: ex?.response,
page_data: json?.page_data,
form_errors: json?.form_errors,
success_message: json?.success_message ?? undefined,
error: json?.form_errors
? (json.form_errors.__all__?.at(0)?.message ??
Object.values(json.form_errors).at(0)?.at(0)?.message ??
'Error')
: undefined,
};
}
return {
raw: ex?.response,
page_data: undefined,
form_errors: undefined,
success_message: undefined,
error: (ex?.toString() || 'Error') as string,
};
} finally {
// The bar stays while any request that shows it is still running, not until the first one ends.
if (commonStore && this.showLoadingIndicator) {
indicatorRequests = Math.max(0, indicatorRequests - 1);
commonStore.isGlobalLoading = indicatorRequests > 0;
}
}
}
protected getDefaultHeaders(nuxt: NuxtApp) {
const headers = import.meta.server
? new Headers(useRequestEvent(nuxt)?.headers)
: new Headers();
if (this.headers) {
Object.entries(this.headers).forEach(([headerName, headerValue]) => {
headers.set(headerName, headerValue!);
});
}
headers.set('X-CSRFToken', readCookieNow('csrftoken') ?? '');
headers.set('Referer', useRequestURL().origin);
headers.set('Origin', useRequestURL().origin);
headers.set('X-TZ', Intl.DateTimeFormat().resolvedOptions().timeZone);
// Documentation pages read the demo storefront (server/api/[...path].ts); a copied page request never decides it.
headers.delete(DEMO_DATA_HEADER);
if (useCommonStore(nuxt.$pinia as import('pinia').Pinia).apiScope === 'demo') {
headers.set(DEMO_DATA_HEADER, '1');
}
// A server render's own call copies the visitor's request headers. A visitor who follows a link from
// another site arrives with `Sec-Fetch-Site: cross-site`; the gateway's cross-site check would refuse the
// render's Application Load and every page would answer 503. The call comes from this server.
if (import.meta.server) {
headers.set('sec-fetch-site', 'same-origin');
headers.set('sec-fetch-mode', 'cors');
headers.set('sec-fetch-dest', 'empty');
}
headers.delete('connection');
return headers;
}
}
export default function useAPI<T = {}>(url: string, method?: TMethod | string) {
return new API<T>(url, method as TMethod);
}
app/functions/api/findLiveApiEndpoint.ts
Lookup of url_name in the current authenticated and public Application Load paths.
import type { IAppLoad } from '@/interfaces/appBase/IAppLoad';
import type { IAppLoadPath } from '@/interfaces/appBase/appLoadCommon/IAppLoadCommonPath';
export function findLiveApiEndpoint(
appLoad: IAppLoad | undefined,
urlName: string,
): { section: 'auth' | 'unauth'; path: IAppLoadPath } | undefined {
for (const section of ['auth', 'unauth'] as const) {
const groups = Object.values(appLoad?.[section]?.paths ?? {});
for (const group of groups) {
const path = Object.values(group).find(
(endpoint) => endpoint.url_name === urlName,
);
if (path) {
return { section, path };
}
}
}
}