Developing an action extension
An action extension is a custom endpoint that can be called from your frontend. It allows you to forward any kind of API calls to backend services, including writing data to it. Especially the latter can't be achieved by a data source extension. The other big differences to a data source extension are:
- An action needs to be triggered manually from the frontend
- Actions can't be configured by Studio users
- The data returned by an action isn't automatically available (especially at Server Side Rendering time)
You can think of an action roughly as a controller that's run for you inside the API hub.
1. Implement the action
Actions are categorized into namespaces for clarity. Namespaces are created by nesting objects in the index.ts
:
export default {actions: {'star-wars': {character: async (request: Request, actionContext: ActionContext): Promise<Response> => {if (!request.query.search) {return {body: 'Missing search query',statusCode: 400,};}return await axios.post('https://frontastic-swapi-graphql.netlify.app/', {query: `{allPeople(name: "${request.query.search}") {totalCountpageInfo {hasNextPageendCursor}people {idnameheighteyeColorspecies {name}}}}`,}).then((response) => {return {body: JSON.stringify(response.data),statusCode: 200,};}).catch((reason) => {return {body: reason.body,statusCode: 500,};});},},}
This action resides in the star-wars
namespace and is named character
.
An action receives 2 parameters:
- The
Request
is a data object that contains selected attributes of the original HTTP (such asquery
holding the URL query parameters) and the session object - The
ActionContext
holds contextual information from the API hub
As the return value, a Response
or a promise returning such, is expected. This response will be passed as the return value to the client. It contains many of the typical response attributes of a standard HTTP response.
We're using the Axios library to perform HTTP requests here. To reproduce this example, you need to add this as a dependency, for example, using yarn add axios
. You can use any HTTP library that works with Node.js, the native Node.js HTTP package, or an SDK library of an API provider.
The action extension in this example receives a URL parameter search
and uses it to find people in the Star Wars API. The result is proxied back to the requesting browser.
2. Use and test the action
Every action is exposed through a URL that follows the schema /frontastic/action/<namespace>/<name>
. So the example action can be reached at /frontastic/action/star-wars/character
. For example, our Frontend component tsx
could look like the below:
import React, { useState } from 'react';import classnames from 'classnames';import { sdk } from 'sdk';type Character = {name: string;height: string;mass?: string;hairColor?: string;eyeColor: string;birthYear?: string;skinColor?: string;gender?: string;homeworld: string;films?: any;species?: any;vehicles?: any;starships?: any;created: string;edited: string;url: string;};type Props = {data: Character[];};const StarWarsCharacterSearch: React.FC<Props> = ({ data }) => {const [inputText, setInputText] = useState('');const [results, setResults] = useState(data);const handleSearchCharacter = () => {sdk.callAction({actionName: 'star-wars/character',query: { search: inputText },}).then((response) => {setResults(response.data.allPeople.people);});};return (<><div className="w-full max-w-xs"><div className="md:flex md:items-center mb-6"><div className="md:w-2/3"><inputid="character"type="text"placeholder="Character"value={inputText}onChange={(e) => {setInputText(e.target.value);}}className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input></div><div className="md:w-1/3"><buttononClick={handleSearchCharacter}className="ml-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"type="button">Search</button></div></div></div>{results.length > 0 && (<div className="bg-white shadow overflow-hidden sm:rounded-lg"><div className="bg-gray-50 px-4 py-5 grid grid-cols-7 sm:gap-4 sm:px-6"><div className="text-sm font-medium text-gray-500">Name</div><div className="text-sm font-medium text-gray-500">Height</div><div className="text-sm font-medium text-gray-500">Eye color</div></div>{results.map((character, i) => (<div key={i} className="border-t border-gray-200"><divclassName={classnames('px-4 py-5 grid grid-cols-7 sm:gap-4 sm:px-6',{'bg-gray-50': i % 2 === 1,})}><div className="mt-1 text-sm text-gray-900 sm:mt-0">{character.name}</div><div className="mt-1 text-sm text-gray-900 sm:mt-0">{character.height}</div><div className="mt-1 text-sm text-gray-900 sm:mt-0">{character.eyeColor}</div></div></div>))}</div>)}{results.length === 0 && <div>Empty list</div>}</>);};export default StarWarsCharacterSearch;
You can test this action using a standard HTTP client. It's essential to send the Accept: application/json
header with your request. For example:
curl -X 'GET' -H 'Accept: application/json' -H 'Commercetools-Frontend-Extension-Version: STUDIO_DEVELOPER_USERNAME' 'https://EXTENSION_RUNNER_HOSTNAME/frontastic/action/star-wars/character?search=skywalker'
For information on the Commercetools-Frontend-Extension-Version
header and the extension runner hostname, see Main development concepts.