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 Frontastic 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 that's run for you inside the Frontastic API hubAPI hub - The API hub combines our code, your code, and APIs to create the backend of a commerce site. Any backend development or extensions are done here. Known as `Catwalk` in code..

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.get<Response>('https://swapi.dev/api/people/?search=' + request.query.search)
        .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 Frontastic session object
  • The ActionContext holds contextual information from the Frontastic 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 Frontastic componentFrontastic component - A customizable building block that's used together with other components to create a commerce site. Known as `tastic` for short in code. tsx could look like the below:

import React, { useState, useEffect } from 'react';
import classnames from 'classnames';

type Character = {
  name: string;
  height: string;
  mass: string;
  hair_color: string;
  eye_color: string;
  birth_year: string;
  skin_color?: string;
  gender: string;
  homeworld: string;
  films?: any;
  species?: any;
  vehicles?: any;
  starships?: any;
  created: string;
  edited: string;
  url: string;
}

type Props = {
  data: Character[];
};

const StarWarsCharacters: React.FC<Props> = ({ data }) => {
  const [inputText, setIputText] = useState('')
  const [results, setResults] = useState(data)

  const OnSearchCharacter = () => {
    fetch(`https://<project_name>-<customer_name>.vercel.app/frontastic/action/star-wars/character?query=${inputText}`)
    .then(response => response.json())
    .then(data => {
      console.log('response data:', data)

      setResults(data.results)
    })
  }

  return (
    <>
      <div className="w-full max-w-xs">
        <div className="md:flex md:items-center mb-6">
          <div className="md:w-2/3">
            <input
              id="character"
              type="text"
              placeholder="Character"
              value={inputText}
              onChange={(e) => { setIputText(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">
            <button
              onClick={OnSearchCharacter}
              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">
              Mass
            </div>
            <div className="text-sm font-medium text-gray-500">
              Height
            </div>
            <div className="text-sm font-medium text-gray-500">
              Gender
            </div>
            <div className="text-sm font-medium text-gray-500">
              Eye color
            </div>
            <div className="text-sm font-medium text-gray-500">
              Hair color
            </div>
          </div>

          {results.map((character, i) => (
            <div key={i} className="border-t border-gray-200">
              <div className={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.mass}
                </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.gender}
                </div>

                <div className="mt-1 text-sm text-gray-900 sm:mt-0">
                  {character.eye_color}
                </div>

                <div className="mt-1 text-sm text-gray-900 sm:mt-0">
                  {character.hair_color}
                </div>
              </div>
            </div>
          ))}
        </div>
      )}

      {results.length === 0 && (
        <div>Empty list</div>
      )}
    </>
  )
}

export default StarWarsCharacters;

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' 'https://<sandbox-host>/frontastic/action/star-wars/character?search=skywalker'

Did this page help you?