Skip to main content

API Reordering(Head API)

API Reordering starts critical API requests from the HTML <head>, before React hydration and page effects run. It is useful when a page's first-screen API normally starts late, but the request parameters are already available during initial page load.

Without API Reordering, the browser usually waits for JavaScript chunks, hydration, provider setup, and page effects before calling the API. With API Reordering, the request can start earlier, and the hydrated client API can relay the already-started result instead of fetching again.

Use this feature when:

  • the API is important for first-screen rendering or perceived loading;
  • the API request currently starts late in the page lifecycle;
  • the request parameters can be derived from URL, cookies, feature control, experiments, locale, currency, or other initial page data.

Avoid this feature when the request depends on React state, React context, user interaction, DOM readiness, or values that only exist after hydration.

How It Works

On SSR, TVLK5 injects a trimmed window.__HEAD_DATA__, the shared headAPI.lib.*.js, and the page-specific __requestBuilder__.*.js into the document head. The request builder runs as soon as the shared library is ready and stores each tracked request in window.headAPIRequests.

When the hydrated page later calls the same API through callAPI, core tries to match that client request with a stored head request. If the match succeeds and the head request resolves successfully, the client receives the head result and skips the normal fetch.

flowchart TD
A[SSR renders page] --> B[Inject __HEAD_DATA__]
B --> C[Load headAPI.lib.js]
C --> D[Load page __requestBuilder__.js]
D --> E[requestBuilder calls request]
E --> F[Store record in window.headAPIRequests]
G[Hydrated page calls callAPI] --> H{Match method and path?}
F --> H
H -- no --> N[Run normal client request]
H -- yes --> I{Match selected payload, domain, and prefix?}
I -- no --> M[Log relay mismatch]
M --> N
I -- yes --> J[Consume matched head record once]
J --> K{Head request success?}
K -- yes --> L[Return relayed result]
K -- no --> O[Log fallback reason]
O --> N

Setup

Step 1: Add a Request Builder

Create a page-level request builder under the owning app:

packages/<domain>/app-<interface>/head-api/<page-path>/__requestBuilder__.tsx

For /hotel/search in webacd-desktop, the file is:

packages/accom/app-desktop/head-api/hotel/search/__requestBuilder__.tsx

Export HeadAPIRequestBuilderEntry[]. The array may contain one or more API requests. Use multiple entries when the page needs more than one head request, especially when each request needs different relay matching logic.

import type {
BuilderContext,
HeadAPIRequestBuilderEntry,
} from '@traveloka/core/api/headAPI/types';

/**
* Function Intent: Build and start the page's first-screen API request from
* the HTML head phase.
* Parameter Breakdown: `request` is the tracked API Reordering request function;
* `context` exposes head-safe page data such as URL params, cookies, currency,
* feature control, and experiments.
* Return Value: Returns the promise from `request`, which core records for
* client-side relay.
* Usage Example: requestBuilder({ request, context }) -> starts `/v2/search`.
*/
function requestBuilder({ request, context }: BuilderContext) {
const params = context.getSearchParams();
const currency = context.getCurrency();

return request({
method: 'post',
domain: 'accomSearch',
path: '/v2/hotel/searchList',
payload: {
data: {
spec: params.get('spec'),
currency,
},
},
});
}

const entry: HeadAPIRequestBuilderEntry[] = [
{
requestBuilder,
},
];

export default entry;

Step 2: Use Head-Safe Context

__requestBuilder__ may import product code to construct payloads, but avoid code that depends on React context, React hooks, provider state, effects, or hydration-time values. API Reordering runs before React context exists.

Use the provided context helpers instead:

HelperPurpose
getSearchParams() / getSearchParam()Read URL search params
getQuery()Read query as an object
getAllCookies()Read document cookies
getCurrency()Read selected currency with fallback
getRoutePrefix()Read locale route prefix
getFeatureControl()Read feature-control values from head data
getExperiments()Read experiment values from head data
getReferrer()Read document.referrer
getPageURL()Read current URL
getClientInterface()Read desktop/mobile interface
getMarketingContext()Build a head-time marketing context snapshot

API Reordering scripts are built separately from the Next.js main bundle. pnpm dev runs this build once when the dev server starts, producing the shared headAPI.lib.*.js runtime and each page-specific __requestBuilder__.*.js bundle under tmp/head-api-sdk. During development, if you change __requestBuilder__ or modules that it imports, rerun the build manually so the head scripts and manifest are refreshed:

pnpm --filter @traveloka/core build:headAPI

Each page path produces its own __requestBuilder__ bundle. The bundled request builder should stay within the configured 40KB CI size limit, so keep imports small and direct, avoid large third-party libraries. After running the build, inspect tmp/head-api-sdk/metafile-iife.json on https://esbuild.github.io/analyze/ to see which source files and dependencies contribute to the output size.

The API Reordering build also shims selected www-only dependencies that are not directly available in the head bundle.

To inspect the source code that API Reordering actually uses during bundling, check the extracted files under /tmp/head-api-extracted.

If product code needs a different implementation in the head bundle, add a .head.js or .head.ts file next to the original file. During API Reordering bundling, the .head file is preferred over the original module. Use this only when the automatic code shim is not enough.

For example, if the original module exports a large class but the request builder only needs two simple methods, create a small head implementation:

SearchPayloadBuilder.ts
SearchPayloadBuilder.head.ts

The request builder can keep importing SearchPayloadBuilder, while the API Reordering bundle resolves to SearchPayloadBuilder.head.ts.

For large third-party libraries, add a simplified replacement under @traveloka/core/api/headAPI/third-party. Use this when the head request only needs a tiny subset of the library behavior, such as a small date formatter or sanitizer helper. This lets the head build resolve to a tiny self-hosted implementation instead of bundling the full third-party library into the request builder.

Step 3: Add Page-Private HEAD_DATA

window.__HEAD_DATA__ has two parts:

  • common data from the core base whitelist;
  • page-private data declared by the page.

The common whitelist includes data such as route prefix, currency, client interface, locale, runtime URL/cookie config, environment, app name, and asset CDN. If the request builder needs extra SSR props, add a sibling __headProps__.ts.

const headProps = {
pageProps: {
rawAppContext: {
featureControl: {
'hotel-default-check-in-date-offset': true,
'hotel-max-stay-duration': true,
accom_search_lcp: true,
},
},
experiments: {
varSearchLCP: true,
varPageSpeed: true,
},
accomSearchDetail: {
monitoringSpec: true,
},
},
};

export default headProps;

These fields are merged into window.__HEAD_DATA__.props for the request builder. Keep this list minimal because it increases the inline HTML payload.

Step 4: Register the Web Service Scope

Add the owning web service to packages/core/api/headAPI/server/scope.ts. This service registration is one of the conditions for API Reordering to run.

const REGISTERED_HEAD_API_SERVICES = new Set<string>([
'webacd-desktop',
'your-web-service',
]);

The service name is the normalized runtime app name. For example, @traveloka/webacd-desktop becomes webacd-desktop.

Once the service is registered, SSR is allowed to inject the shared headAPI.lib.*.js runtime for pages in that service. When this runtime loads in the HTML <head>, it auto-starts preflight(), so SSG token preflight and update-token work can happen earlier, before the normal hydrated client API flow starts.

Registering the service alone does not guarantee that a page-specific __requestBuilder__ script is injected. The page still needs a manifest entry and the head-api feature control below.

Step 5: Enable Feature Control

Enable the page through the head-api feature control. The property key is a readable name; the value is the Next.js page path.

{
"head-api": {
"enabled": true,
"properties": {
"hotelSearch": "/hotel/search"
}
}
}

The request builder script is injected only when the service is registered, the page has a manifest entry, and the feature control includes the current page path.

Relay Rules

Each tracked head request stores:

  • request options passed to request;
  • the request promise;
  • optional payloadSelector.

Relay is attempted when the hydrated client calls callAPI. Matching is done in two stages:

  1. Compare method and path.
  2. Compare selected payload, domain, and prefix.

By default, the full head payload must deeply equal the full client payload. If the payload contains values that differ between head and client phases, add payloadSelector and return only stable fields.

payloadSelector must not mutate the payload argument. The same payload may still be used by the hydrated client request, so selector authors are responsible for keeping the original payload unchanged. When removing unstable fields, shallow-copy the relevant object first, then trim fields from the copy.

/**
* Function Intent: Select stable payload fields for API Reordering relay matching.
* Parameter Breakdown: `payload` is the payload from either the head request
* or the hydrated client request.
* Return Value: Returns the comparable payload fragment.
* Usage Example: payload with `{ data: { geoId: "1", tid: "random" } }` ->
* `{ data: { geoId: "1" } }`.
*/
function payloadSelector(payload: unknown): unknown {
const data = (payload as { data?: Record<string, unknown> } | undefined)
?.data;
if (!data) return payload;

// Copy before trimming so the original request payload stays unchanged.
const stableData = { ...data };
delete stableData.tid;
delete stableData.isJustLogin;

return { data: stableData };
}

const entry: HeadAPIRequestBuilderEntry[] = [
{
requestBuilder,
payloadSelector,
},
];

Relay is one-shot. Once a head request matches a client API call, it is removed from window.headAPIRequests. If the head request succeeds, the relayed result is returned. If it fails or rejects, the client falls back to the normal request path.

When method/path match but payload, domain, or prefix differ, core logs API Reordering relay match failed with mismatch details. If payloadSelector throws, core logs Failed to select head API relay payload. If the matched head request fails, core logs API Reordering relay request failed; falling back to client request.

For multiple API Reordering requests, each request can relay independently. A single successful relay is enough for the page-level TTI report to count API Reordering as relayed.

TTI Reporting

TTI logs include a headAPI value:

ValueMeaning
nullThe page has no enabled API Reordering data.
skip_headAPI Reordering is enabled, but it is skipped during request build stage.
no_relayAPI Reordering is enabled, but no head request was successfully relayed.
relayAt least one head request was successfully relayed to the hydrated client call.

For pages with multiple API Reordering requests, the report is relay if any one request relays successfully.

Debugging

Use these checks in dev or staging:

  • window.__HEAD_DATA__.headAPI.enabled: whether the page emitted an enabled API Reordering payload.
  • window.headAPIRequests: queued head requests that have not been consumed by relay.
  • window.headAPIRelayUsed: set to true after a successful relay.
  • Network panel: check headAPI.lib.*.js, page __requestBuilder__.*.js, and the target API request.
  • ?nohead: skip page request builder execution in development and staging without disabling the shared API Reordering library preflight.

If the request builder script is missing, check service registration, the generated manifest, the page path in feature control, and whether the request builder bundle was generated under the expected head-api/<page-path> directory.