Skip to content

API-Driven UI Configuration: Stop Hardcoding Your Dashboard

Every dashboard has summary cards at the top. “Total Users: 142”, “Active: 98”, “At Risk: 12”. Product loves them. Users click them constantly.

Then Product wants changes. “Add a ‘High Cost’ card.” That’s a frontend deploy. “Change the ‘At Risk’ threshold from 5 to 10.” Another deploy. “Enterprise clients want different cards.” Now you’re adding feature flags. “A/B test which cards drive engagement.” Good luck.

The pattern: UI configuration that changes frequently is hardcoded in the frontend. Every change requires a deploy. Every customer variation requires conditional logic.

The fix: treat UI configuration as data, not code. Fetch it from an API. Backend owns the config. Frontend renders whatever it gets.

+-----------------------------------------------------------+
|               HARDCODED CONFIG                            |
|                                                           |
|   const CARDS = [                                         |
|     { label: 'Total', key: 'total' },                     |
|     { label: 'Active', key: 'active' },                   |
|   ];                                                      |
|                                                           |
|   Want to add a card? --> Deploy frontend                 |
|   Want to change threshold? --> Deploy frontend           |
|   Enterprise wants different cards? --> Feature flags     |
+-----------------------------------------------------------+

+-----------------------------------------------------------+
|               API-DRIVEN CONFIG                           |
|                                                           |
|   GET /api/ui-config                                      |
|   {                                                       |
|     "summaryCards": [...],                                |
|     "facets": [...],                                      |
|     "thresholds": {...}                                   |
|   }                                                       |
|                                                           |
|   Backend owns it. Frontend renders it.                   |
+-----------------------------------------------------------+

Now adding a card is a config change. Different cards per customer is a database row. A/B testing is backend logic. Frontend deploys for UI changes drop to zero.


The Core Idea

// Don't hardcode
const CARDS = [{ label: "Total", icon: UsersIcon }];

// Fetch
const { config } = useDashboardConfig("users");
// config.summaryCards, config.facets, config.thresholds

Backend owns the config. Frontend renders whatever it gets.


The API Contract

interface UIConfig {
  users: EntityUIConfig;
  products: EntityUIConfig;
}

interface EntityUIConfig {
  summaryCards: SummaryCardConfig[];
  facets: FacetConfig[];
  thresholds: Record<string, number>;
}

interface SummaryCardConfig {
  key: string; // matches data key
  label: string;
  icon: string; // string key, not component
  variant: "default" | "success" | "warning" | "error";
}

Example Response

{
  "users": {
    "summaryCards": [
      {
        "key": "total",
        "label": "Total Users",
        "icon": "users",
        "variant": "default"
      },
      {
        "key": "active",
        "label": "Active",
        "icon": "check-circle",
        "variant": "success"
      },
      {
        "key": "atRisk",
        "label": "At Risk",
        "icon": "alert-triangle",
        "variant": "warning"
      }
    ],
    "facets": [
      { "key": "status", "label": "Status" },
      { "key": "role", "label": "Role" }
    ],
    "thresholds": {
      "atRisk": 5,
      "highCost": 1000
    }
  }
}

The Hook

const FALLBACK: UIConfig = {
  users: {
    summaryCards: [
      { key: "total", label: "Total", icon: "users", variant: "default" },
    ],
    facets: [],
    thresholds: {},
  },
};

function useDashboardConfig(tab: keyof UIConfig) {
  const { data, isError } = useQuery({
    queryKey: ["ui-config"],
    queryFn: () => fetch("/api/ui-config").then(r => r.json()),
    staleTime: 5 * 60 * 1000, // 5 min
  });

  const isFallback = isError || !data;

  if (isFallback) {
    console.warn(`Using fallback config for ${tab}`);
  }

  return {
    config: data?.[tab] ?? FALLBACK[tab],
    isFallback,
  };
}

Key decisions:

  • staleTime: 5 min: Config rarely changes. Don’t refetch every mount.
  • Always return config: Never undefined. Fallback is mandatory.
  • Log fallback: Know when API is failing.

Icons as String Keys

Icons can’t be serialized. Use a registry:

const ICONS = {
  users: Users,
  "check-circle": CheckCircle,
  "alert-triangle": AlertTriangle,
};

function DynamicIcon({ name }: { name: string }) {
  const Icon = ICONS[name] ?? Circle;
  return <Icon className="w-4 h-4" />;
}

API returns "icon": "alert-triangle". Frontend maps it.


Rendering Cards

function SummaryCards({ config, data, onCardClick }) {
  return (
    <div className="grid grid-cols-4 gap-4">
      {config.map(card => (
        <button
          key={card.key}
          onClick={() => onCardClick(card.key)}
          className={VARIANT_STYLES[card.variant]}
        >
          <DynamicIcon name={card.icon} />
          <span>{card.label}</span>
          <span>{data[card.key]?.toLocaleString() ?? "—"}</span>
        </button>
      ))}
    </div>
  );
}

Component doesn’t know what cards exist. Renders whatever config says.


Fallback Strategy

The API will fail. Plan for it.

Layer 1: React Query retry (2 attempts)
Layer 2: Stale-while-revalidate (serve cached for 5 min)
Layer 3: Hardcoded fallback (always available)
Layer 4: UI indicator (show banner when using fallback)
Layer 5: Monitoring (track fallback usage)
{
  isFallback && <Banner variant="warning">Using default configuration.</Banner>;
}

When to Skip This

  • Config never changes: API overhead not worth it
  • No backend team: Just deploy together
  • Complex conditional logic: 50 business rules? Put them in code

Works when:

  • Different customers need different configs
  • Product iterates on dashboard without deploys
  • A/B testing UI elements

Results

Three dashboards:

  • “Add a card” went from 2-hour deploy to 5-minute config change
  • Enterprise clients get their own config, no code branches
  • “What config is this user seeing?” is one API call
  • Config API had 2 outages. Users never noticed (fallbacks).

Dev Tools

if (typeof window !== "undefined") {
  window.__uiConfig = data;
  window.__clearUIConfigCache = () => {
    queryClient.invalidateQueries(["ui-config"]);
  };
}

In console:

__uiConfig; // See current config
__clearUIConfigCache(); // Force refetch

Your dashboard config is data. Stop deploying to change a label.