API Calls
There are two kind of API call that you can do. Universal data fetching, and client-side only API call.
API definition convention
This was what was originally suggested during the time of writing. For new products/packages, we suggest you to take a look at how Flight engineers define API constants (@traveloka/fpr-common/meta).
To help with API definition, we have the convention to put all API related definition in api.ts in root directory of your package.
First we define the path, with API_{DOMAIN} prefix. Next, we add the type definition of the API response and request (the request type definition is optional).
// packages/platform/app-mobile/api.ts
export const API_USER_LIKES = '/v2/user/likes';
export type UserLikesAPIResponse = {
data: {
likes: Array<string>;
};
};
// If you want you can also annotate your request
export type UserLikesAPIRequest = {
data: {
userId: string;
};
};
Universal data fetching
This is exactly the same as Next.js data fetching: you can use getStaticProps, getServerSideProps, or getInitialProps to call the API.
We've provided you with the callAPI function that accepts (1) the context object and (2) the API configuration (like domain and path).
On the server-side (during SSR), this will hit the backend endpoint directly. Meanwhile, on the client-side (including when we are doing client-side navigation using the legacy getInitialProps), it will hit our API proxy (i.e. the webapx service).
// packages/platform/app-mobile/pages/user/index.tsx
import { TravelokaPageComponent, callAPI } from '@traveloka/core/next';
import { API_USER_LIKES, UserLikesAPIResponse } from '../../api';
type Props = {
results: Array<string>,
};
const Page: TravelokaPageComponent<Props> = props => {
// do something with props.results
return <div />;
};
// using getStaticProps
export const getStaticProps = staticProps({
async getStaticProps(ctx) {
const res = await callAPI(ctx, {
method: 'post',
domain: 'user',
path: API_USER_LIKES,
payload: {
data: {
userId: ''
}
}
})
}
});
// using getServerSideProps
export const getServerSideProps = async ctx => {
const res = await callAPI(ctx, {
method: 'post',
domain: 'user',
path: API_USER_LIKES,
payload: {
data: {
userId: ''
}
}
});
});
// using getInitialProps
Page.getInitialProps = async ctx => {
const res = await callAPI(ctx, {
method: 'post',
domain: 'user',
path: API_USER_LIKES,
payload: {
data: {
userId: ''
}
}
});
}
Client-side API call
Client-side API call doesn't always equal to data fetching: you can hit an API and don't care about the response, or you can also do data fetching. Here, we provide the useAPI hook that returns a function that you can execute whenever you wish.
Fire and forget
Usually this is used for tracking purpose where you need to hit an API after doing something and doesn't need to care about the response. You can use useAPI or useSendBeacon.
(The Beacon API is used to send an asynchronous and non-blocking request to a web server. The browser guarantees to initiate and complete beacon requests before the page is unloaded.)
import { useAPI, useSendBeacon } from '@traveloka/core';
function Component(props) {
const fetch = useAPI({
method: 'post',
domain: 'data',
path: '/v2/monitor/log',
});
const sendBeaconLogList = useSendBeacon('data', '/v1/tvlk/events');
function handlePress() {
// Using fetch
fetch();
// Using beacon
sendBeaconLogList({
data: {
myPayload: 123,
myOtherPayload: 456,
},
});
}
return <Button onPress={handlePress} text="Click Me!"></Button>;
}
Data fetching
To add data fetching, you can compose this hooks with something like swr or react-query. (Or you can read the response object returned by your fetch function.)
import useSWR from 'swr';
import { useAPI } from '@traveloka/core';
function Component(props) {
const fetch = useAPI({
method: 'post',
domain: 'user',
path: API_USER_LIKES,
});
const { data, error } = useSWR('cacheKey', fetch);
if (error) {
return `Error ${error.message}`;
}
if (!data) {
return 'loading...';
}
return data;
}
Hooks-less API call
The reason why client-side API call is exposed via hooks is because by default, it also handles MFA/OTP error during the API call and integrates seamlessly with the framework to provide retry after the user has filled their OTP.
In some cases, it's probably unnecessary and makes it hard to create patterns like fetch-as-render or integrate with state management side-effect (like calling API inside redux action). If you encounter this case, you can sidestep this "limitation" by using callAPI and passing an empty context as its first argument.
How do you know you can safely use this method? If you're able use this inside getInitialProps without error in client-side navigation, chances are you can use the same method outside getInitialProps.
import { NextPageContext } from 'next';
import { callAPI } from '@traveloka/core';
const ctx = {} as NextPageContext;
function action() {
return async dispatch => {
const data = await callAPI(ctx, {
method: 'post',
domain: 'user',
path: API_USER_LIKES,
});
dispatch({
type: 'RECEIVE_DATA',
data,
})
}
}
Additonal API service: Client State
Client-state is a service that stores client data on our service (useful for example, when the browser does not support storage, like in Safari incognito).
To interact with client state, you can use the fetchClientState function that accepts a path string and a data object.
import { fetchClientState } from '@traveloka/core';
function Component(props) {
const [bookingData, setBookingData] = React.useState(null);
React.useEffect(() => {
fetchClientState('/path/to/get', { key: props.bookingId }).then(data => {
setBookingData(data);
});
}, []);
if (!bookingData) {
return 'loading...';
}
return <BookingDataRenderer {...bookingData} />;
}
Additonal API service: Partner API
Another kind of API interaction you might encounter is how to interact with Partner APIs. These are APIs implemented using JWT authentication and doesn't use our API proxy mechanism.
To call these API, there's a usePartnerAPI hook that returns both low-level utilities and ready-to-use function.
Most of the time, you can simply use the fetch function returned from the hook, but you can also compose its lower-level primitives using fetchWithRetries, isTokenExpired, and getPartnerContext. Even though these are exported, it's not recommended to always build one yourself, so we don't provide any docs. You'll need to read PartnerAPI.ts source code yourself.
import { usePartnerAPI } from '@traveloka/core';
function Component(props) {
const partnerAPI = usePartnerAPI({
// usually you read this from publicRuntimeConfig
clientId: 'clientID',
apiHost: 'https://api.host',
});
const {
// lower-level features
isTokenExpired,
getPartnerContext,
fetchWithRetries,
// main function
fetch,
} = partnerAPI;
React.useEffect(() => {
const payload = { key: 'value' };
fetch('/api/path', payload).then(res => {
console.log('res', res);
});
}, []);
return null;
}
Hooks-less Partner API call
If you want to interact with Partner API on server, you can use the hooks-less version named callPartnerAPI. The difference between callPartnerAPI and usePartnerAPI is that callPartnerAPI will store the token that's not expired yet on cookie, while usePartnerAPI will store the token on local storage.
export const getServerSideProps = async ctx => {
const res = await callPartnerAPI(ctx, {
apiHost: 'https://api.host',
apiPath: GET_TRANSACTION_DETAILS,
clientId: '123',
data: {
transactionId: '123',
},
});
};
Server-Sent Events (SSE)
Server-Sent Events (SSE) allow the server to push data to the client over a single HTTP connection. This is useful for real-time updates like streaming responses, live notifications, or progress updates.
useSSE Hook
The useSSE hook provides a React-friendly way to establish and manage SSE connections. It handles connection lifecycle, automatic reconnection, and cleanup.
import { useSSE } from '@traveloka/core';
function StreamingComponent() {
const [messages, setMessages] = useState<string[]>([]);
const { isConnected, error, connect, disconnect } = useSSE({
domain: 'flight',
path: '/v1/flight/stream-prices',
payload: { searchId: '123' },
onMessage: data => {
setMessages(prev => [...prev, data]);
},
onError: err => {
console.error('SSE error:', err);
},
reconnect: true,
maxReconnectAttempts: 5,
});
return (
<div>
<p>Connected: {isConnected ? 'Yes' : 'No'}</p>
{error && <p>Error: {error.message}</p>}
<button onClick={connect}>Connect</button>
<button onClick={disconnect}>Disconnect</button>
<ul>
{messages.map((msg, i) => (
<li key={i}>{msg}</li>
))}
</ul>
</div>
);
}
Options
| Option | Type | Default | Description |
|---|---|---|---|
domain | string | required | The API domain (e.g., 'flight', 'accomSearch') |
path | string | required | The API endpoint path |
payload | object | {} | Request payload |
onMessage | function | required | Callback when a message is received |
onError | function | - | Callback when an error occurs |
onOpen | function | - | Callback when connection is established |
onClose | function | - | Callback when connection is closed |
reconnect | boolean | true | Whether to automatically reconnect on failure |
reconnectInterval | number | 3000 | Delay between reconnection attempts (ms) |
maxReconnectAttempts | number | 5 | Maximum number of reconnection attempts |
autoConnect | boolean | true | Whether to connect automatically on mount |
Return Values
| Value | Type | Description |
|---|---|---|
isConnected | boolean | Whether the connection is currently open |
error | Error | null | The last error that occurred |
connect | function | Function to establish the connection |
disconnect | function | Function to close the connection |
closeAllSSEConnections
A utility function to close all active SSE connections. This is useful for cleanup during page navigation or when the user logs out.
import { closeAllSSEConnections } from '@traveloka/core';
// Close all SSE connections when user logs out
function handleLogout() {
closeAllSSEConnections();
// ... other logout logic
}