API Routes
Next.js provides us with an API routes that we can leverage as an API proxy to transform original API response from backend to a format more suitable for frontend. This can improve your web performance as you ship less JS to client because you moved the logic to API routes.
As API routes is part of next.js feature, it's also have built-in hot reload enabled so you can develop any API requirement easily.
Setup
Your API routes' base path in each services will be prefixed, as top-level path /api is reserved for API proxy. For example, platform team will have /platform/api as it's base path. You can see your product prefix in the prefix field in the package.json of your service package (app-mobile/app-desktop)
Even though each service will have its own prefix, in practice you don't have to put the file inside a prefix directory. For example, in platform if you want to create /platform/api/some/path you can simply create platform/app-mobile/pages/api/some/path.ts. This routing logic is automatically handled by your express server
Because an API routes is bound to specific service (either mobile, or desktop), if you want to hit them in both desktop and mobile, you have to create both files in desktop and mobile api directory.
Note that if you have unique prefix (like platform) that hasn't been registered to ALB, you also need to submit a terraform change plan to our production ALB, as well as changes to route.yml.
Server-side Helpers
To ease the creation of API routes there are some helpers available for you under @traveloka/core/api imports, notably: setCookie and callAPI.
// packages/platform/app-mobile/pages/api/some/path.ts
import { setCookie, callAPI } from '@traveloka/core/api';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const options = {};
setCookie(res, 'key', 'value', options);
const res = await callAPI(req, {
domain: 'content',
path: '/v2/mobile/batchapidata',
method: 'post',
payload: {
contentResource: {},
},
});
return res.json({ success: res.success });
}
Type-safe router
Going even further you can create custom router (similar to express) in your API routes so you can easily separate GET and POST requests so we don't create a POSTful API. This custom router also comes with a type narrowing capability to help you handle request and response.
Unfortunately the request is not validated. If you want it to be, you have to validate it against the type yourself.
import { Router } from '@traveloka/core/api';
const router = Router();
router.get((req, res) => {
// req & res is automatically typed
res.cookie('key', 'value', options);
});
// type narrowing
type RequestType = {
name: string;
};
type ResponseType = {
hello: string;
};
router.post<RequestType, ResponseType>((req, res) => {
req.body.name; // string
res.json({ hello: 123 }); // type error
});
Note that if you use this router, any HTTP method not currently handled will result in 405 status code.
Client-side Helpers
To fetch an API routes, you can simply do it in an effect / event handlers using native fetch function.
You can also leverage useAPI to do the data fetching for you so you can easily customize timeout and client-side caching mechanism. Pass prefix instead of domain to useAPI to hit API routes instead of API proxy. You can pass any prefix, or if you want to hit your own API routes, you can retrieve your API route prefix through productPrefix property inside publicRuntimeConfig.
import { getPublicRuntimeConfig, useAPI } from '@traveloka/core';
function Component() {
// this will fetch `/platform/api/some/path` in platform service
const fetch = useAPI({
prefix: getPublicRuntimeConfig().productPrefix,
path: '/some/path',
method: 'post',
});
useEffect(() => {
fetchAPI();
}, []);
}
You can also use callAPI if you want the hook-less version.
Testing
To test an API route, you can use APIRoutes helper from @traveloka/core/test
import { APIRoutes } from '@traveloka/core/test';
import handler from '../some/path';
test('success GET', async () => {
const { body, status, headers } = await APIRoutes.get(handler, {
headers: {
// custom header as key-value pair
},
query: {
// query string as key-value pair
},
cookies: {
// cookie as key-value pair
},
});
expect(status).toBe(200);
expect(headers['set-cookie']).toBe('key=value');
});
test('success POST', async () => {
const { body, status, headers } = await APIRoutes.post(handler, {
payload: {
name: 'lol',
},
});
expect(status).toBe(200);
expect(body.json().hello).toBe('123');
});