RPC methods
Overview
Storefront Core is built on top of a set of so-called RPC Methods. They are JavaScript function that follow a certain interface and are registered as RPC methods. These RPC methods allow us to package calls into business logic-driven methods and expose them through an automatically generated API. This way, developers do not have to spend time writing and calling REST APIs, but can quickly use that functionality inside their application. Since we are using Typescript, type support for parameters and responses is provided out of the box.
Universal rendering
Nuxt offers a variety of rendering modes with the most typical being Universal Rendering. With Universal rendering, the Vue components are rendered both on the server and the client.
However, RPC methods will only be executed server-side. If a call to an RPC method is made while rendering server-side, the RPC method's function will be called directly. When an RPC method is called from client-side rendering, an HTTP request will be made to execute the RPC method on the server and respond with the result.
This is all handled automatically and the API to call RPC methods does not differ. In Storefront Core we make all API calls to SCAYLE within RPC methods. This means that the calls are only made server-side and we do not expose API tokens to the client and certain implementation details remain private.
Rendering a page on the server
useProduct
is a composable that can be invoked during the Server-Side Rendering or on the client. It in turn depends on the getProduct
RPC Method. Since getProduct
is executed on the server, it has access to the API token and can call the Storefront API. Additionally, most RPC methods utilize the cache module, reducing latency and load on Storefront API.
Write an RPC method
Storefront applications can also add their own RPC Methods which can then be called in the application. These RPCs also have the same access to the RPCContext which provides information about the session, the shop and sensitive tokens.
Writing an custom RPC should be done for logic which calls another service, either a SCAYLE system or a third party provider.
Additionally it can also be used to ensure sensitive code/data doesn't get bundled into the client.
To get started, simply create a new file in the rpcMethods
directory of your project:
import type { RpcContext, RpcHandler } from '@scayle/storefront-nuxt'
// RPC Method without params
export const getShopId = async function getShopId(context: RpcContext) {
return context.shopId
} satisfies RpcHandler<number>
Here we create a new RPC called getShopId
which will return the shop identifier provided through the context.
The RPC also receives no input and it satisfies the RPC contract.
It is important that we export our RPC function here, and we also must export this function from the rpcMethods/index.ts
file. To do so, add the following line to the file:
// This will export all RPC functions from the shop.ts file
export * from './shop'
Receive Arguments for your RPC
Some of the RPCs may also require to receive parameters. To pass them, add the expected parameters as the first argument in your RPC method.
The RpcContext
is still provided into the second argument of the RPC.
Additionally we need to add this type to the satisfies RpcHandler<Input, Output>
code for the type definitions.
import type { RpcContext, RpcHandler } from '@scayle/storefront-nuxt'
// RPC Method with parameters
export const subscribeToNewsletter = async function subscribeToNewsletter(
params: { email: string },
_: RpcContext,
) {
// TODO: Subscribe the user to your newsletter
return params.email
} satisfies RpcHandler<{ email: string }, string>
We now have an RPC method called subscribeToNewsletter
that receives an email and subscribes it to your newsletter. Currently, it returns the email as the result of the RPC.
Override RPC Methods
Since Storefront Core also includes its own set of RPCs, there is a possibility of name conflicts, which could result in accidentally overriding an existing RPC method.
In this case you will receive a warning in your console indicating which RPC method was overridden, as it can lead to unexpected side effects and cause the application to behave improperly.
However, in certain situations it may be necessary to override RPC methods from Storefront Core. This should be done with extreme caution and only when absolutely necessary.
To not spam the console if this is a case, you can add the overridden RPC method to the storefront.rpcMethodOverrides
configuration in your nuxt.config.ts
.
This will silence this warning for the specific RPC method.
If an RPC method is overridden, we cannot guarantee that the Storefront Core will work as expected. It becomes your responsibility to test all relevant cases.
Calling RPC Methods
Now that we have written our custom RPC Methods, we need some way of calling them from our composables or components.
Depending on your use case and requirements, you have two options for executing your RPC:
1. useRpc: This provides a declarative approach to calling the RPC within a page, component, or composable.
2. useRpcCall: This should be used when the RPC is triggered in response to an event or user interaction, rather than during the initial setup.
Both useRpc and useRpcCall are composables and must follow composable usage rules.
It may also help to familiarize yourself with the Data fetching guide from Nuxt. useRpc
is based on useAsyncData and should be used in a similar manner, while rpcCall
is more like $fetch.
useRpc
useRpc
offers a declarative way to do data fetching in your application. It manages the fetch state, automatically calls the RPC method and returns you the result or an error.
A lot of the composables exported by Storefront Core also rely on useRpc
and and serve as light wrappers around the RPCs we provide.
Let's say we want to call our getShopId
RPC from our previous example:
<template>
<div>
{{ fetching ? 'Loading...' : shopId }}
</div>
</template>
<script setup lang="ts">
import { useRpc } from '#storefront/composables'
const { data: shopId, fetching } = useRpc('getShopId', 'current-shop-id')
</script>
The first argument to useRpc
is our RPC Method name which is just the name under which the RPC is exported.
The second argument is a unique key
which Nuxt uses to prevent refetching the same data and to repopulate the SSR data. Learn more about this here.
We generally recommend making the key unique to the location and passing this data explicitly around instead of relying on the assumption that the same key is used somewhere and will not trigger a refetch of the data.
The third argument are the parameters for an RPC if it has any, in our example, we don't have to pass any parameters.
The argument can receive a reactive function or a computed value, which will automatically retrigger a refetch if any of the data in the parameters changes.
<template>
<div>
{{ fetching ? 'Loading...' : content.title }}
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRpc } from '#storefront/composables'
const props = defineProps<{ id: number }>()
// Let's imagine we have a custom RPC called `getCmsContent` which takes an ID parameter
// We pass this as a computed value to automatically refetch the data in case the ID changes
const { data: content, fetching } = useRpc(
'getCmsContent',
`cms-content-${props.id}`,
computed(() => ({ id: props.id })),
)
</script>
Handling of shared state
By default, useRpc
utilizes a shared cache, causing all instances called with the same key to share the same data. You can disable this behaviour globally by setting the disableDefaultGetCachedDataOverride
option in the public runtime config.
export default defineNuxtConfig({
runtimeConfig: {
public: {
storefront: {
disableDefaultGetCachedDataOverride: true,
}
}
}
})
However you can also disable this on a single call to useRpc
:
<template>
<div>
{{ fetching ? 'Loading...' : content.title }}
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRpc } from '#storefront/composables'
const props = defineProps<{ id: number }>()
const { data: content, fetching } = useRpc(
'getCmsContent',
`cms-content-${props.id}`,
computed(() => ({ id: props.id })),
{ getCachedData: undefined }
)
</script>
If it is disabled globally, it can be also enabled for certain useRpc
call:
<template>
<div>
{{ fetching ? 'Loading...' : content.title }}
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRpc } from '#storefront/composables'
const props = defineProps<{ id: number }>()
const { data: content, fetching } = useRpc(
'getCmsContent',
`cms-content-${props.id}`,
computed(() => ({ id: props.id })),
{
getCachedData: (key, nuxtApp) => {
const hydrationData = nuxtApp.isHydrating
? nuxtApp.payload.data[key]
: nuxtApp.static.data[key]
return hydrationData ?? nuxtApp._asyncData[key]?.data.value
},
},
)
</script>
useRpcCall
useRpcCall
is designed to be used when you need to call an RPC in response to events or user interactions, such as clicking a button to add a product to the basket or subscribing to a newsletter.
We can use again our example subscribeToNewsletter
RPC which is triggered when the user submits the newsletter form.
<script setup lang="ts">
import { useRpcCall } from '#storefront/composables'
import { ref } from 'vue';
const subscribeToNewsletter = useRpcCall('subscribeToNewsletter')
const email = ref('[email protected]')
const onSubmit = async () => {
const res = await subscribeToNewsletter({ email: email.value })
console.log(res)
}
</script>
useRpcCall
takes the RPC Method name and returns a callback. When this callback is invoked, it receives the parameters for the RPC and makes the request. It returns a promise that resolves with the response from the RPC.
It then returns a promise which will resolve to the response provided by the RPC.
RPC Context
The RPC context object provides information about the current application, application secrets and helpers for RPCs.
Some of the most important properties and methods include:
Property Name | Description |
---|---|
sapiClient | A reference to the Storefront API client initialized to the shop of the request. |
campaignKey | The key for the currently active campaign. By default, we will choose any active campaign at the time of the request. |
cached | A utility function for caching functions. |
user | The current user. |
wishlistKey | The wishlist identifier for the current session. |
basketKey | The basket identifier for the current session. |
sessionId | The unique session id of the current user. |
shopId | The shop id of the current request. |
log | A reference to the logger. |
runtimeConfiguration | An object containing the private properties set at runtime through the environment. |
Extend RPCContext
Your RPC methods may require additional information which can also be added to the RPC Context to make it available in all of your RPCs.
This can be achieved by hooking into the storefront:context:created
nitro runtime hook, where you can add properties to the RPC context or overwrite existing ones.
To do this, create a server plugin in which you can register a hook with nitroApp.hooks.hook('storefront:context:created', handler)
.
The handler is a sync/async function that receives the base RPCContext
as its argument.
By setting properties or changing property values of the RPCContext
you can change the context passed to RPC methods.
Overwriting existing properties of the RPC Context is not recommended. It might break parts of the application and therefore needs to be done very carefully!
Adding a new property
To add a new property to the PRC Context, use the following snippet:
import { defineNitroPlugin } from 'nitropack/runtime/plugin'
// Augment RpcContext type to make sure our application is also aware of the property
declare module '@scayle/storefront-nuxt' {
interface AdditionalRpcContext {
myNewProp: string
}
}
// Set new value on rpcContext
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('storefront:context:created', (rpcContext) => {
rpcContext.myNewProp = 'My new context value'
})
})
Overwrite existing values
It is possible to overwrite or change existing values in the RPC Context. However, this should be carefully and we generally do not recommend making such changes.
If it is absolutely necessary, follow this approach:
import { defineNitroPlugin } from 'nitropack/runtime/plugin'
// overwriting
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('storefront:context:created', (rpcContext) => {
rpcContext.campaignKey = 'My campaign key'
})
})