store-api-proxy
powered by Nitropack
should be understood as a thin-layer on top of the Shopware' Store-API. It provides us with a easy-to-maintain way to leverage Vercel' data cache for caching (and later on transform as well as orchestrate) API responses to overcome the shortcomings of the Store API we're consuming.
- Resolving sales channel based on request header
sw-access-key
- Automatically switching to development enviroment using
x-env
header - Nitro' filesystem routing for API routes
- Conventional Commits support using
husky
as pre-commit hook - Type-safe using Schema definitions from Shopware Frontends API client
- Code quality checks using
ESLint
&Prettier
- Deployment on Vercel, boosted using
Turborepo
git checkout https://github.com/KoRoHandelsGmbH/store-api-proxy
cd store-api-proxy
npm install
The store-api-proxy is providing a thin-layer on top of the Store-API which is getting shipped with each and every sales channels created within our Shopware instance. Depending on the sales channel a different sw-access-key
request header has to be provided within the incoming request.
Based on the sw-access-key
the proxy is getting the right target url. If an additional sw-language-id
is getting passed within the request headers it'll respect the header and forwards the request to the Store-API.
Routes we would like to be cached are provided as their own API route using a defineCachedEventHandler()
. These event handler allowing a second argument for cache control. Usually we want to cache the response in Vercel' data cache for 60 minutes and provide a stale response while invalidating:
{
maxAge: 60 * 1 * 60,
swr: true,
varies: ['sw-access-key', 'sw-language-id'],
}
When the incoming request to the store-api-proxy contains the header x-env
and the content of the header is either dev
or development
we're switching the target url from the production servers to the integrations server.
We're using Nitro' filesystem routing for providing additional event handlers for API routes. Each and every route has to be placed into the folder server/routes/store-api
. The right target url will be terminated based on the sw-access-key
header of the incoming request.
When your route doesn't contain additional route parameters (e.g. for example POST /language
, GET /payment-method
) we simply provide the last part of the URL as the file name.
Original url: /store-api/language
File path: server/routes/store-api/language.ts
When we're dealing with routes which are containing additional route parameters (e.g. for example POST /navigation/{navigationId}/{navigationId}
) we're using a catch-all routes:
Original url: /store-api/navigation
File path: server/routes/store-api/navigation/[...].ts
After creating the necessary file using the filesystem routing we're providing a defineCachedEventHandler()
including the basic cache configuration:
export default defineCachedEventHandler(
async (event) => {},
{
maxAge: 60 * 1 * 60,
swr: true,
varies: ['sw-access-key', 'sw-language-id'],
},
);
Next up, it's time to fill out the event handler body. For a convenient and easy-to-use way to get the necessary information from the incoming request you can use the helper method usePrepareRequest(event: H3Event)
. It's important here to provide the H3Event
as parameter. The event contains the context, response and request.
Last but not least, we have to fire the request and providing the error reporting using createError
.
const { url, requestOptions } = await usePrepareRequest(event);
try {
const response = await $fetch(url, requestOptions);
return response;
} catch (err) {
throw createError(err);
}
In the end your event listener should look like this:
import { usePrepareRequest } from '~~/utils/usePrepareRequest';
export default defineCachedEventHandler(
async (event) => {
const { url, requestOptions } = await usePrepareRequest(event);
try {
const response = await $fetch(url, requestOptions);
return response;
} catch (err) {
throw createError(err);
}
},
{
maxAge: 60 * 1 * 60,
swr: true,
varies: [
'sw-access-key',
'sw-language-id',
'x-env',
'sw-include-seo-urls',
],
},
);
Additionally it's possible to transform the response before sending it to the client. Let's assume we're within the route /{locale}/store-api/navigation/{navigationId}/{navigationId}
.
Within your event handler body you should find a code snippet like this:
const response = await $fetch(url, requestOptions);
return response;
Before we're able to transform the response we have to define the type of the response. For that the store-api-proxy provides the entitiy schemas from Shopware Frontends API client.
First you're importing the Schemas
type from the globally available module #shopware
;
import type { Schemas } from '#shopware';
Next up, you map the response to the correct entity schema. In this example we're getting back an array category
const response: Schemas['Category'][] = await $fetch(url, requestOptions);
return response;
Now, you can iterate over response using Array.prototype.map()
and override the properties you don't need:
const response: Schemas['Category'][] = await $fetch(
url,
requestOptions,
);
return response.map((item) => {
item.description = '';
if (item.translated) {
item.translated.description = '';
}
return item;
});
The method useSalesChannel()
is a fundamental part of the stack resolving. It's reading out the runtime config which contains the information about the available sales channels, gets the sw-access-key
from the request stack and returns the right target url based on the sw-access-key
.
The method usePrepareRequest()
prepares the request to be send by the proxy. It reads the body and headers of the incoming request, sanitizes the path using useSanitizedPath()
, provides the correct sw-access-key
using useSalesChannel()
as well as the necessary headers and body for the request to be sent to the Store API.
You can start the development sever after a successful npm install
using:
npm run dev
This spawns up the Nitro on the port 3000
. A different port can be provided using a .env
file with the following content:
PORT=9210
After doing the changes you wanted to do, please commit your changes using a commit message following the conventional commit guidelines.