Convert a JSON-based card to a full-stack UI extension (BETA)

  • Sales Hub
    • Enterprise
  • Service Hub
    • Enterprise

If you previously used HubSpot projects to build a CRM card with JSON-based components, learn how to convert it to a full-stack UI extension. By converting the card, you can take advantage of the additional flexibility and interactivity that come with full-stack UI extensions. 

Below, learn more about the differences between JSON-based cards and full-stack UI extensions, how to think about conversion, and see an example of converting the previous ZenQuotes cards to function with React.

  • JSON-based cards: custom cards built with JSON-based components. These cards use serverless functions to fetch data and build card components on the same layer. This results in the card not being able to respond to different states as the user interacts with it.
  • Full-stack extensions: extensions in the form of custom cards that are built with React as a frontend framework and serverless functions for fetching and process data. This results in a more dynamic card that can take advantage of states for a more interactive and data-rich experience.

Before getting started

When converting your card, keep in mind the following key differences between JSON-based cards and full-stack UI extensions:

  • With full-stack UI extensions, you can think about your front-end and back-end needs separately. In this sense, the UI extension can be treated like a micro-app
  • Full-stack UI extensions offer a richer component library with options to make them as interactive as possible using React.
  • Full-stack UI extensions support more layout management options through the Flex and Box components, similar to using CSS flexbox.
  • Full-stack UI extensions enable complete local development support by showing your front end changes in HubSpot without needing to reload the page. You can also take advantage of enhanced debugging options in the browser console and IDE debugger with console.log() for a more streamlined building process.

With these differences in mind, it's strongly recommended to use the conversion process as an opportunity to rethink your card's capabilities and overall user experience. Rather than simply rebuilding the card to the new system, you should consider how you'd ideally like the card to function without the constraints that come with JSON-based cards.


At a high level, you'll convert your card as follows:

Before you begin:

  • Think about your desired user experience, keeping in mind that your UI extension can support more flexibility and interactivity. 
  • Identify your needs for fetching data using APIs. You'll now be able to split your code for your frontend and backend needs.
  • Learn more about how full-stack UI extensions work.

Then, get started with full-stack UI extensions:

  • Update to the latest version of the CLI by running npm install -g @hubspot/cli@next. 
  • Follow the quickstart guide to create your first UI extension.
  • After completing the quickstart guide and familiarizing yourself with the new toolset, you can also check out some of HubSpot's example extensions for inspiration.
  • Finally, create a new boilerplate project by running hs project create. Based on your learnings, build your re-imagined UI extension card. 

As an example, below you'll find an example of converting the ZenQuotes card to be a full-stack UI extension

Example conversion: ZenQuotes card

Previously, HubSpot provided an example card that fetched and displayed quotes from ZenQuotes

zenquotes-cardThis card is powered by a single JavaScript file containing a serverless function:

const axios = require("axios"); exports.main = async (context = {}, sendResponse) => { const { firstname } = context.propertiesToSend; const { data } = await axios.get(""); const quoteSections = [ { type: "tile", body: [ { type: "text", format: "markdown", text: `**Hello ${firstname}, here's your quote for the day**!`, }, { type: "text", format: "markdown", text: `_${data[0].q}_`, }, { type: "text", format: "markdown", text: `_**Author**: ${data[0].a}_`, }, ], }, { type: "button", text: "Get new quote", onClick: { type: "SERVERLESS_ACTION_HOOK", serverlessFunction: "crm-card", }, }, ]; sendResponse({ sections: [...quoteSections] }); };

To rebuild this card using the boilerplate project, you'll need to make a few updates to your app and card's config files, then split the serverless function code between the serverless function and a new frontend React file.

Update the app.json file

In the app.json file, you'll need to add a uid value for the app, which is the app's unique identifier. This can be any string value, and enables you to change the name of the app itself without losing any historical data such as logs. For example, you could set a uid of getting_started_example_app.

// app.json { "name": "Get started app (React)", "description": "This is an example of private app that shows a custom card on the Contact record tab built with React-based frontend.", "scopes": ["", "crm.objects.contacts.write"], "uid": "getting_started_example_app" "public": false, "extensions": { "crm": { "cards": [ { "file": "extensions/example-card.json" } ] } } }

Updates to the example-card.json file

Previously, the card's JSON config file was contained in the app folder. Now, however, this file is kept within the app/extensions folder, which will also contain the eventual frontend React code among other files. 

You no longer need to include the fetch object or targetFunction. Instead, you'll need to include a uid to identify the extension, along with a module object to specify the React file.

// example-card.json { "type": "crm-card", "data": { "title": "Example Card", "uid": "getting_started_example", "location": "", "module": { "file": "Example.jsx" }, "objectTypes": [{ "name": "contacts" }] } }

Add a serverless function to fetch quotes

The serverless.json file will be configured as before. However, the serverless function is now dedicated to fetching quotes on the back end, rather than also rendering components.

// serverless.json { "runtime": "nodejs16.x", "version": "1.0", "appFunctions": { "get-quotes": { "file": "get-quotes.js", "secrets": [] } } }

Next, update the custom-card.js file name to match the file you specified above (get-quotes.js). Then, update the serverless function code as follows. Note that you're no longer fetching propertiesToSend in this file. Instead, you'll be adding that functionality to the React file.

const axios = require("axios"); exports.main = async (context = {}, sendResponse) => { const { data } = await axios.get(""); try { sendResponse(data); } catch (error) { sendResponse(error); } };

Create the React frontend

In the src/app/extensions folder, the Example.jsx file will handle the frontend code for your extension. This file name needs to match the one specified in the card's JSON config file (in data.module). 

This .jsx file will call the get-quotes function, then display that data using components. Additionally, the code below includes a few simple UX improvements, such as an empty state.


import React, { useState, useEffect } from "react"; import {hubspot,Text,Button,Flex,EmptyState,Heading,} from "@hubspot/ui-extensions"; const FetchQuotes = ({ runServerless, fetchCrmObjectProperties }) => { const [quotes, setQuotes] = useState([]); const [firstName, setFirstName] = useState(""); useEffect(() => { fetchCrmObjectProperties(["firstname"]).then((response) => { setFirstName(response.firstname); }); }, []); const fetchQuotes = async () => { runServerless({ name: "get-quotes" }).then((resp) => setQuotes(resp.response) ); }; if (quotes.length > 0) return ( <Flex direction="column" gap="medium" align="center"> { => ( <> <Heading> Quote of the day {firstName ? `for ${firstName}` : ""} </Heading> <Text format={{ italic: true, fontWeight: "bold" }} >{`${quote.q}`}</Text> <Text variant="microcopy">- by {`${quote.a}`}</Text> </> ))} <Button onClick={fetchQuotes}>Get new quote</Button> </Flex> ); else { return ( <Flex direction="column" gap="medium" align="center"> <EmptyState title="No Quotes Found" layout="vertical" imageWidth={100} /> <Button onClick={fetchQuotes}>Get new quote</Button> </Flex> ); } }; hubspot.extend(({ runServerlessFunction, actions }) => ( <FetchQuotes runServerless={runServerlessFunction} fetchCrmObjectProperties={actions.fetchCrmObjectProperties} /> ));

Note that this uses a different approach for fetching CRM properties. With full-stack UI extensions, you can use fetchCrmObjectProperties to fetch CRM properties on the React client side. This is an alternative approach, since you could still include propertiesToSend inside the serverless function to fetch properties on the backend, which can be better suited when needing the most up-to-date data. Learn more about fetching CRM properties.

Was this article helpful?
This form is used for documentation feedback only. Learn how to get help with HubSpot.