RPC methods
Overview
The Storefront Core is built upon a foundation of Remote Procedure Call (RPC) methods. These are JavaScript functions that adhere to a defined interface and are registered with the RPC system. This approach allows us to encapsulate business logic into reusable functions, exposed through an automatically generated API. As a result, developers can easily integrate this functionality without the overhead of building and managing REST APIs. TypeScript provides automatic type checking for parameters and return values.
Universal rendering
Nuxt offers several rendering modes, the most common being Universal Rendering. This mode renders Vue components on both the server and the client. However, RPC methods are executed only on the server. During server-side rendering, calling an RPC method directly executes its corresponding function. On the client-side, calling an RPC method triggers an HTTP request to the server, which executes the method and returns the result.
This process is handled automatically, providing a consistent API for calling RPC methods regardless of the rendering context. In Storefront Core, all calls to SCAYLE are made within RPC methods. This server-side execution protects API tokens from client exposure and keeps certain implementation details private.
Rendering a page on the server
Consider the useProduct
composable, which can be used during both server-side rendering and on the client. It relies on the getProduct
RPC method. Because getProduct
executes on the server, it has access to the API token and can call the Storefront API. Furthermore, most RPC methods leverage caching to reduce latency and minimize the load on the Storefront API.
Storefront Core API example
Custom RPC Methods
Storefront applications can define custom RPC methods, callable within the application. These custom RPCs have access to the RPCContext, providing information about the session, shop, and secure tokens.
Creating a custom RPC method is recommended when integrating with external services, such as SCAYLE add-ons or third-party providers. This approach also helps prevent sensitive code and data from being exposed to the client.
To create a custom RPC method, simply add a new file to 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>
This example demonstrates creating an RPC method called getShopId
, which returns the shop identifier from the context. This RPC takes no input arguments and fulfills the RPC contract.
It's crucial to export the getShopId
function from its file and from the rpcMethods/index.ts
file. To export from rpcMethods/index.ts
, add the following line:
// This will export all RPC functions from the shop.ts file
export * from './shop'
Passing Arguments to RPC Methods
RPC methods can accept parameters. Define these parameters as the first argument of your RPC function. The RpcContext
remains the second argument. You'll also need to update the satisfies RpcHandler<Input, Output>
declaration to reflect the input and output types.
This example defines the subscribeToNewsletter
RPC method, which accepts an email address and (currently) returns it after subscribing the user to a newsletter. The RpcHandler
type declaration specifies the input type ({ email: string }
) and the output type (string
).
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>
Registering a custom RPC method
In order for a custom RPC method to be usable, it must be registered. Registration will create the necessary endpoint for the RPC and extend the types to include the RPC. The process of registering an RPC method changes depending on whether you are creating a custom RPC method within the storefront application or as part of a Nuxt module.
Within a Storefront Application
In order to register an RPC method in a storefront application, you must define rpcDir
and rpcMethodNames
in the module configuration. rpcDir
defines the directory from where the method functions will be imported, and rpcMethodNames
is a list of the identifiers to import and register as RPC methods.
{
rpcDir: 'rpcMethods',
rpcMethodNames: ['customRpcMethod']
}
Overriding Core 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.
Within a Nuxt module
Nuxt modules can also register custom RPC methods. In order for a module to register a custom RPC method, they should register for the storefront:custom-rpc:extend
hook and append their RPC method import definitions. An import definition includes the source file and the export names to register as RPC methods.
export default defineNuxtModule({
setup(_options, nuxt) {
const resolver = createResolver(import.meta.url)
nuxt.hook('storefront:custom-rpc:extend', async (customRpcs) => {
customRpcs.push({
source: await resolver.resolvePath('./rpc-methods'),
names: ['foo', 'bar'],
})
})
},
})
The registration of RPC methods via this hook is slightly more restrictive. If a hook-registered RPC method uses the same name as an application-registered RPC method, the application-registered one will have precedence. RPC methods registered via the hook are not allowed to override core methods.
Source | Priority | Can override core methods? | Location |
---|---|---|---|
Core RPC Method | Medium | n/a | storefront-core package |
storefront:custom-rpc:extend | Low | No | Nuxt module (or application) |
`rpcDir`/`rpcMethodNames` | Highest | Yes | Application |
Calling Custom RPC Methods
After defining your custom RPC methods, you need a way to call them from your components or composables. Two options are available, depending on your needs:
- useRpc: This composable provides a declarative approach for calling RPCs within pages, components, or other composables. It's suitable for calls that should occur during initial setup.
- useRpcCall: Use this composable when triggering an RPC in response to an event or user interaction.
Both useRpc and useRpcCall are composables and must follow composable usage rules. For further guidance, refer to Nuxt's data fetching guide. useRpc
behaves similarly to useAsyncData
, while useRpcCall
is more akin to $fetch
.
useRpc
useRpc
offers a declarative approach to data fetching. It handles the fetch state, automatically executes the RPC method, and returns the result or any errors. Many Storefront Core composables utilize useRpc
as lightweight wrappers around provided RPCs.
Here's how to call the getShopId
RPC from the 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 the RPC method name (the name under which the RPC is exported). The second argument is a unique key, which Nuxt uses to avoid redundant fetches and repopulate server-side rendered data. For more details, consult the Nuxt documentation.
We recommend using location-specific unique keys and explicitly passing data rather than assuming key usage and fetch behavior elsewhere.
The third (optional) argument provides parameters to the RPC method. In this example, getShopId
doesn't require parameters. If parameters are needed, you can pass a reactive function or a computed value. useRpc
will automatically re-fetch the data if any of the reactive parameters change.
<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>
Managing Shared State with useRpc
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,
}
}
}
})
You can also disable this behavior for individual useRpc
calls:
<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 disabled globally, you can re-enable shared caching for specific useRpc
calls:
<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 for calling RPCs in response to events or user interactions, such as clicking a button or submitting a form. Consider the subscribeToNewsletter
example, triggered by a form submission:
<script setup lang="ts">
import { useRpcCall } from '#storefront/composables'
import { ref } from 'vue';
const subscribeToNewsletter = useRpcCall('subscribeToNewsletter')
const email = ref('hello@scayle.com')
const onSubmit = async () => {
const res = await subscribeToNewsletter({ email: email.value })
console.log(res)
}
</script>
useRpcCall
accepts the RPC method name and returns a callback function. When invoked, this callback receives the RPC parameters, makes the request, and returns a promise that resolves with the RPC's response.
RPC Context
The RPC context object provides information about the current application, including application secrets and helper functions for RPCs. Key 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. |
Extending the RPC Context
You can add custom information to the RPC context to make it available to all your RPC methods. This is achieved by hooking into the storefront:context:created
Nitro runtime hook.
Create a server plugin and register a hook using nitroApp.hooks.hook('storefront:context:created', handler)
. The handler
is a synchronous or asynchronous function that receives the base RPCContext
as its argument. You can modify the context passed to RPC methods by adding or changing properties on this object.
Caution: Overwriting existing RPCContext
properties is strongly discouraged, as it can break application functionality. Proceed with extreme care if you choose to do so.
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
While possible, overwriting or changing existing values within the RPC context is generally not recommended and should be done with extreme caution. If absolutely necessary, use the following approach:
import { defineNitroPlugin } from 'nitropack/runtime/plugin'
// overwriting
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('storefront:context:created', (rpcContext) => {
rpcContext.campaignKey = 'My campaign key'
})
})