Extending the Middleware
When developing your storefront, there will be times when you need to customize the way the middleware and integrations run. For example, you might want to add or edit a new API endpoint or even modify the Express.js application itself. Alokai's Server Middleware allows you to do this using extensions.
Adding an extension
Extensions can be added to any integration in your middleware.config.ts
file by creating an extensions
function. This function receives an array of existing extensions as the only argument and has to return an updated array of extensions (with your extension added).
export const integrations = {
sapcc: {
// ...
extensions: (extensions) => [
...extensions, // don't forget to include existing extensions
{
name: 'extension-name',
isNamespaced: `[true/false:default]`,
extendApiMethods: { /* ... */ },
extendApp: (app) => { /* ... */ },
hooks: () => { /* ... */ }
}
]
}
}
Creating an extension
You can define as many extensions as you want. Each extension has the following structure:
const extension = {
name: "extension-name",
isNamespaced: `[true/false:default]`,
extendApiMethods: {
customMethod: (context, params) => {
/* ... */
},
},
extendApp: (app) => {
/* ... */
},
hooks: (req, res) => {
return {
beforeCreate: ({ configuration }) => configuration,
afterCreate: ({ configuration }) => configuration,
beforeCall: ({ configuration, callName, args }) => args,
afterCall: ({ configuration, callName, args, response }) => response,
};
},
};
name
- a unique name for your extension,isNamespaced
- defines if the extension should be namespaced. Namespaced extensions are registered under/{integration-name}/{extension-name}
extension of integration's namespace in opposition to non-namespaced extensions which are registered under/{integration-name}
integration's namespace. Default value isfalse
. Extensions without a namespace can potentially override existing endpoints, so it's recommended to use namespaced extensions whenever possible.extendApiMethods
- overrides an integration's API Client to modify default behavior or add new API endpointsextendApp
- gives you access to the Express.js apphooks
- defines lifecycle hooks of API-clientbeforeCreate
- called before API-client creates a connection. It accepts an integration configuration as an argument and must return it as well. You can use it to modify the configuration or merge it with the default values,afterCreate
- similar to the previous function, but called after the connection has been created. It accepts an integration configuration as an argument and must return it as well. This hook is usually used for cleanup work after altering the configuration inbeforeCreate
,beforeCall
- called before each API-client function. Gives you access to the integration configuration, function name, and arguments. Can be used to modify the arguments based on the input parameters and must return them,afterCall
- called after each API-client function. Gives you access to the configuration, function name, arguments and response. Can be used to modify the response based on the input parameters and must return it.
Lifecycle Hooks
Middleware extensions allow you to extend your Express.js server, register additional API endpoints, or hook into the lifecycle of a request sent to a given Server Middleware integration from the application.
Use Cases
Adding an Endpoint
To register a new API endpoint, you can register a custom extension and use the extendApiMethods
property. API
endpoints cannot be registered directly. Let's look at an example:
export const integrations = {
sapcc: {
// ...
extensions: (extensions) => [
...extensions,
{
name: "example-extension",
extendApiMethods: {
baseSites: async (context) => {
// Using integration's HTTP client to make a request to SAP Commerce Cloud backend
// SAPCC integration is using `axios` as an HTTP client, so we want to retreive only the `data` property from the response.
const { data } = await context.client.get("/basesites");
return data;
},
},
},
],
},
};
All integrations can be extended, however, this example extends the SAP Commerce Cloud integration, to give more context about real-life usage. We registered baseSites
in extendApiMethods
which creates a new /api/sapcc/baseSites
endpoint.
This method accepts two parameters:
context
- integration context which includes:config
- integration configurationclient
- HTTP client used by the integrationreq
- HTTP request objectres
- HTTP response objectextensions
- extensions registered within an integrationcustomQueries
- custom GraphQL queries registered within integration (used only with GraphQL)extendQuery
- helper function for handling custom queries (used only with GraphQL)
params
- parameters passed in the request's body
Using extension methods in the frontend
To call the extension endpoint, you should use the middlewareModule.
First, you need to prepare the type definition for the extension methods. It will be necessary to pass this type to the middlewareModule
. To do it, let's aggregate the extension methods in a single object:
export const extensionMethods = {
baseSites: async (context, params) => {
const { data } = await context.client.get("/basesites");
return data;
},
};
export const integrations = {
sapcc: {
location: "@vsf-enterprise/sapcc-api/server",
configuration: {
// ...
},
extensions: (extensions) => [
...extensions,
{
name: "example-extension",
extendApiMethods: {
...extensionMethods,
},
},
],
},
};
Then, let's use the WithoutContext
type helper to prepare the type of the endpoints created by the extension methods. As a good practice, it's recommended to use a separate file for the types used in the middleware configuration:
import { type WithoutContext } from "@vue-storefront/middleware";
import { extensionMethods } from "./middleware.config";
export { type Endpoints as SapccEndpoints } from "@vsf-enterprise/sapcc-api";
export type ExtensionEndpoints = WithoutContext<typeof extensionMethods>;
Finally, pass the ExtensionEndpoints
type the middlewareModule
type parameter:
import { CreateSdkOptions, createSdk } from "@vue-storefront/next";
import { SapccEndpoints, ExtensionEndpoints } from "storefront-middleware/types";
const options: CreateSdkOptions = {
middleware: {
apiUrl: "http://localhost:4000",
},
};
export const { getSdk } = createSdk(
options,
({ buildModule, middlewareUrl, middlewareModule, getRequestHeaders }) => ({
sapcc: buildModule(middlewareModule<SapccEndpoints & ExtensionEndpoints>, {
apiUrl: middlewareUrl + "/sapcc",
defaultRequestConfig: {
headers: getRequestHeaders(),
},
}),
})
);
Now, the SDK is aware of the extension methods, and you can call them as follows:
const baseSites = await sdk.sapcc.baseSites();
Creating a configurable extension
In more complex cases, you might want to create an extension that can be configured.
To avoid code duplication, you can prepare a closure that will create a new extension with a given configuration.
Using closures is a recommended way to create a configurable extension. It allows you to reuse the same logic in multiple integrations and keeps your code clean and maintainable.
const createCacheControlExtension = (config: Record<string, string>) => {
return {
name: "cache-control-extension",
hooks(req, res) {
return {
afterCall({ response, callName }) {
if (req.method !== "GET") {
console.log("[CACHING] It's not a GET request, skipping caching");
return response;
}
if (res.getHeader("set-cookie")) {
console.log(
"[CACHING] Response containing Set-Cookie header, skipping caching"
);
return response;
}
if (callName in config) {
res.set("Cache-Control", config[callName]);
}
return response;
},
};
},
};
};
Then, you can use it in your middleware.config.ts
file:
export const integrations = {
sapcc: {
// ...
extensions: (extensions) => [
...extensions,
createCacheControlExtension({
searchProducts: "public, max-age=3600",
}),
],
},
contentful: {
// ...
extensions: (extensions) => [
...extensions,
createCacheControlExtension({
getEntries: "public, max-age=3600",
}),
],
},
};