# 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: 1. An action needs to be triggered manually from the frontend 2. Actions can't be configured by Studio users 3. 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](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) 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`: ```typescript title="backend/index.ts" export default { actions: { 'star-wars': { character: async (request: Request, actionContext: ActionContext): Promise => { 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}") { totalCount pageInfo { hasNextPage endCursor } people { id name height eyeColor species { 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 as `query` 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](https://github.com/axios/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//`. The example action can be reached at `/frontastic/action/star-wars/character`. For example, our Frontend component `tsx` could look like the below: ```typescript title="frontend/star-wars/character-search/index.tsx" 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 = ({ 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 ( <>
{ 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" >
{results.length > 0 && (
Name
Height
Eye color
{results.map((character, i) => (
{character.name}
{character.height}
{character.eyeColor}
))}
)} {results.length === 0 &&
Empty list
} ); }; 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: ```shell 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](/frontend-development/extensions.md#extension-runner). ## Calling an action **Prerequisites** Read the [creating a Frontend component article](/frontend-development/creating-a-frontend-component.md). To trigger an operation on a backend system or fetch data asynchronously after initial rendering, call an action extension. For more information, see [Developing an action extension](/frontend-development/developing-an-action-extension.md). For this example, we'll use the following Frontend component to implement searching a backend data set using an action: ```typescript title="Implementing the character search Frontend component" import React from 'react'; const CharacterSearchTastic: React.FC = () => { return ( <>
{ event.preventDefault(); }} >
    ); }; export default CharacterSearchTastic; ``` ```json title="Character search Frontend component schema" { "tasticType": "example/character-search", "name": "Character search", "category": "Example", "description": "A frontend component showing actions and session handling", "schema": [] } ``` The Frontend component doesn't provide any configuration to the Studio and only renders a rudimentary search form and an empty list. ## Perform the fetch call An action extension is a server function in the API hub that can be invoked by a URL in the following format: `https:///frontastic/action//`. For more information about action endpoints, see [Action](/frontend-api/action.md). To communicate with your custom action extensions, use the frontend SDK's [`callAction`](/frontend-development/using-the-frontend-sdk.md#callaction) method as it automatically resolves the correct API hub host for you, maintains the session, and provides other configuration options. ```typescript title="Implementing the character search action call" import { sdk } from 'sdk'; export const characterSearch = async (search: string) => { sdk .callAction({ actionName: 'star-wars/character', query: { search: encodeURIComponent(search) }, }) .then((response) => { setResults(response.data.allPeople.people); }); }; ``` ### Send custom headers You can send only the `coFE-Custom-Configuration` custom header with your request to the API hub; all other custom headers are ignored by the API hub. You can pass a string value to this header using the SDK's [`callAction`](/frontend-development/using-the-frontend-sdk.md#callaction) `customHeaderValue` parameter, as demonstrated in the following example: ```ts title="An example of sending custom headers to actions" import { sdk } from 'sdk'; export const characterSearch = async (search: string) => { return await sdk .callAction({ actionName: 'star-wars/character', query: { search: encodeURIComponent(search) }, customHeaderValue: 'header-value-as-string', }) .then((response) => { setResults(response.data.allPeople.people); }); }; ``` It is not possible to set a response header from your extensions. ## Related pages - [Area overview page with navigation](/frontend-development.md) - [Previous page: Developing a data source extension](/frontend-development/developing-a-data-source-extension.md) - [Next page: Developing a dynamic page extension](/frontend-development/developing-a-dynamic-page-extension.md)