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

  • Sales Hub
    • Enterprise
  • Service Hub
    • Enterprise

By March 31, 2024, JSON-based cards will no longer be supported and you will not be able to update them. If you're currently still using JSON-based cards, review this article to convert them to full-stack UI extensions before the sunset date. 

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. These cards will be fully deprecated by March 31, 2024.
  • 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.

Key differences

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.

Conversion process overview


Start the conversion process by:

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


After planning, 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 HubSpot's sample projects 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 recreating the ZenQuotes card as 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 was powered by a single JavaScript file containing a serverless function, as shown in the code below. Over the course of the next few sections, you'll walk through how to recreate this same functionality as a React-based UI extension with a separate back end and front end. 

// Example JSON-based card code for reference 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 build this card using the boilerplate project, you'd need to make a few updates to your project, app, and card config files, then split the serverless function code between the serverless function and a new frontend React file.

Update the project.json file

In the project.json file, you'll need to add a platformVersion value, which tells HubSpot which version of the developer platform you're building on. The current version is 2023.2.

// project.json { "name": "my_project", "srcDir": "src", "platformVersion": "2023.2" }

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": "nodejs18.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 = {}) => { const { data } = await axios.get(""); try { return (data) } catch (error) { return (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.