docs
  1. Developer Guide
  2. Technical Foundation
  3. Rpc Methods

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.

SourcePriorityCan override core methods?Location
Core RPC MethodMediumn/astorefront-corepackage
storefront:custom-rpc:extendLowNoNuxt module (or application)
`rpcDir`/`rpcMethodNames`HighestYesApplication

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:

  1. 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.
  2. 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 NameDescription
sapiClientA reference to the Storefront API client initialized to the shop of the request.
campaignKeyThe key for the currently active campaign. By default, we will choose any active campaign at the time of the request.
cachedA utility function for caching functions.
userThe current user.
wishlistKeyThe wishlist identifier for the current session.
basketKeyThe basket identifier for the current session.
sessionIdThe unique session id of the current user.
shopIdThe shop id of the current request.
logA reference to the logger.
runtimeConfigurationAn 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'
  })
})
Provide Feedback