Add server/routes/icons.ts:

import { Router } from "express";
import { sanitizeSvg } from "../util/sanitize";
import { generateSvgFromPrompt } from "../util/svg-gen";

const router = Router();

/**
 * POST /api/icons/generate
 * body: { prompt: string, style?: "modern"|"classic"|"flat"|"outlined"|"solid"|"handdrawn"|"isometric"|"material" }
 * returns: { svg: string }
 */
router.post("/generate", async (req, res) => {
  try {
    const { prompt, style = "outlined" } = req.body || {};
    if (!prompt || typeof prompt !== "string" || prompt.trim().length < 2) {
      return res.status(400).json({ error: "Missing or invalid 'prompt'." });
    }
    const allowed = new Set([
      "modern",
      "classic",
      "flat",
      "outlined",
      "solid",
      "handdrawn",
      "isometric",
      "material",
    ]);
    const styleKey = allowed.has(String(style)) ? String(style) : "outlined";

    // Local generator (fast, offline) — returns valid <svg>
    const rawSvg = generateSvgFromPrompt(prompt, styleKey as any);

    // Safety pass
    const svg = sanitizeSvg(rawSvg);

    // Hard stop if sanitization fails
    if (!svg.startsWith("<svg") || svg.length > 32_000) {
      return res.status(422).json({ error: "Invalid SVG generated." });
    }

    return res.json({ svg });
  } catch (e: any) {
    console.error("[/icons/generate] error", e);
    return res.status(500).json({ error: "Failed to generate icon." });
  }
});

export default router;


Add server/util/sanitize.ts:

// Very conservative sanitizer for inline icons.
export function sanitizeSvg(input: string): string {
  let s = input.trim();

  // Remove XML headers and DOCTYPEs
  s = s.replace(/<\?xml[^>]*>/gi, "");
  s = s.replace(/<!DOCTYPE[^>]*>/gi, "");

  // Disallow script / foreignObject
  s = s.replace(/<script[\s\S]*?<\/script>/gi, "");
  s = s.replace(/<foreignObject[\s\S]*?<\/foreignObject>/gi, "");

  // Strip event handlers (onload, onclick, etc.)
  s = s.replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, "");
  s = s.replace(/\son[a-z]+\s*=\s*'[^']*'/gi, "");
  s = s.replace(/\son[a-z]+\s*=\s*[^ >]*/gi, "");

  // Remove external references to remote URLs in xlink:href/href
  s = s.replace(/\s(xlink:)?href\s*=\s*"https?:[^"]*"/gi, "");
  s = s.replace(/\s(xlink:)?href\s*=\s*'https?:[^']*'/gi, "");

  // Ensure viewBox exists
  if (!/viewBox=/.test(s)) {
    s = s.replace(
      /<svg([^>]*)>/i,
      (_m, g1) => `<svg${g1} viewBox="0 0 24 24">`
    );
  }

  // Force width/height removal so it scales in your UI
  s = s.replace(/\swidth\s*=\s*"[^"]*"/gi, "");
  s = s.replace(/\sheight\s*=\s*"[^"]*"/gi, "");

  // Tighten: keep only a small set of tags and attributes
  // (Lightweight; for stricter needs, consider a dedicated SVG sanitizer lib.)
  return s;
}


Add server/util/svg-gen.ts:

type StyleKey =
  | "modern"
  | "classic"
  | "flat"
  | "outlined"
  | "solid"
  | "handdrawn"
  | "isometric"
  | "material";

/**
 * Simple local generator:
 * - Parses prompt keywords (e.g., rocket, star, chart, heart, user, home, folder)
 * - Emits SVG in requested style
 * - Always uses currentColor for easy theming
 * - Ensures viewBox 0 0 24 24
 *
 * You can replace this with an LLM-backed generator later.
 */
export function generateSvgFromPrompt(prompt: string, style: StyleKey): string {
  const p = prompt.toLowerCase();

  const shape =
    p.includes("rocket") ? "rocket" :
    p.includes("star") ? "star" :
    p.includes("chart") || p.includes("bar") ? "barchart" :
    p.includes("heart") ? "heart" :
    p.includes("user") || p.includes("person") || p.includes("profile") ? "user" :
    p.includes("home") || p.includes("house") ? "home" :
    p.includes("folder") ? "folder" :
    p.includes("shield") ? "shield" :
    p.includes("link") ? "link" :
    "circle";

  // Style configuration
  const cfg = styleToConfig(style);

  // Emit a chosen base path/group
  const inner = iconFor(shape, cfg);

  // Wrap in SVG
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"${cfg.svgAttrs}>${inner}</svg>`;
}

function styleToConfig(style: StyleKey) {
  switch (style) {
    case "solid":
      return {
        svgAttrs: ` fill="currentColor"`,
        stroke: `none`,
        fill: `currentColor`,
        strokeWidth: 0,
        rounded: false,
      };
    case "flat":
      return {
        svgAttrs: ` fill="currentColor"`,
        stroke: `none`,
        fill: `currentColor`,
        strokeWidth: 0,
        rounded: false,
      };
    case "handdrawn":
      return {
        svgAttrs: ``,
        stroke: `currentColor`,
        fill: `none`,
        strokeWidth: 2,
        rounded: true,
        dash: "2 2",
      };
    case "isometric":
      return {
        svgAttrs: ``,
        stroke: `currentColor`,
        fill: `none`,
        strokeWidth: 2,
        rounded: false,
      };
    case "material":
      return {
        svgAttrs: ` fill="currentColor"`,
        stroke: `none`,
        fill: `currentColor`,
        strokeWidth: 0,
        rounded: false,
      };
    case "classic":
      return {
        svgAttrs: ``,
        stroke: `currentColor`,
        fill: `none`,
        strokeWidth: 2,
        rounded: false,
      };
    case "modern":
      return {
        svgAttrs: ``,
        stroke: `currentColor`,
        fill: `none`,
        strokeWidth: 2,
        rounded: true,
      };
    case "outlined":
    default:
      return {
        svgAttrs: ``,
        stroke: `currentColor`,
        fill: `none`,
        strokeWidth: 2,
        rounded: false,
      };
  }
}

function iconFor(
  shape: string,
  cfg: { stroke: string; fill: string; strokeWidth: number; rounded: boolean; dash?: string }
): string {
  const capjoin = cfg.rounded ? ` stroke-linecap="round" stroke-linejoin="round"` : ``;
  const dash = cfg.dash ? ` stroke-dasharray="${cfg.dash}"` : ``;

  switch (shape) {
    case "rocket":
      return cfg.fill === "currentColor"
        ? `<path d="M12 2l4 2c1.5.8 2.5 2.3 2.5 4l-3.5 3.5L10.5 6.5 12 2Z" fill="${cfg.fill}"/><path d="M6 18l2-2m-1 3l1-1" fill="none" stroke="currentColor" stroke-width="${cfg.strokeWidth}"${capjoin}/>`
        : `<g fill="${cfg.fill}" stroke="${cfg.stroke}" stroke-width="${cfg.strokeWidth}"${capjoin}${dash}>
             <path d="M14 4.1c1.14-.07 2.28.28 3.16 1.16c.88.88 1.23 2.02 1.16 3.16L14.6 12l-4.6-4.6L14 4.1Z"/>
             <path d="M5 19l3-3m-1 4l2-2"/>
             <path d="m8.28 13.72l2 2"/>
             <circle cx="15" cy="9" r="1"/>
           </g>`;
    case "star":
      return cfg.fill === "currentColor"
        ? `<path d="m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77 6 21.02 7 14.14 2 9.27l6.91-1.01z" fill="${cfg.fill}"/>`
        : `<path d="m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77 6 21.02 7 14.14 2 9.27l6.91-1.01z" fill="none" stroke="${cfg.stroke}" stroke-width="${cfg.strokeWidth}"${capjoin}${dash}/>`;
    case "barchart":
      return cfg.fill === "currentColor"
        ? `<g><rect x="3" y="10" width="3" height="10" rx="1" fill="${cfg.fill}"/><rect x="9" y="6" width="3" height="14" rx="1" fill="${cfg.fill}"/><rect x="15" y="3" width="3" height="17" rx="1" fill="${cfg.fill}"/></g>`
        : `<g fill="none" stroke="${cfg.stroke}" stroke-width="${cfg.strokeWidth}"${capjoin}${dash}><rect x="3" y="10" width="3" height="10" rx="1"/><rect x="9" y="6" width="3" height="14" rx="1"/><rect x="15" y="3" width="3" height="17" rx="1"/></g>`;
    case "heart":
      return `<path d="M12 20s-6-3.3-8.5-6.5C1 10.5 3 6 6.5 6C9 6 10.5 8 12 9.5C13.5 8 15 6 17.5 6C21 6 23 10.5 20.5 13.5C18 16.7 12 20 12 20Z" fill="${cfg.fill}" stroke="${cfg.stroke}" stroke-width="${cfg.strokeWidth}"${capjoin}${dash}/>`;
    case "user":
      return `<g fill="${cfg.fill}" stroke="${cfg.stroke}" stroke-width="${cfg.strokeWidth}"${capjoin}${dash}><circle cx="12" cy="8" r="4"/><path d="M4 20c0-3.314 3.582-6 8-6s8 2.686 8 6"/></g>`;
    case "home":
      return cfg.fill === "currentColor"
        ? `<path d="M12 3l9 8h-3v9H6v-9H3l9-8z" fill="${cfg.fill}"/>`
        : `<g fill="none" stroke="${cfg.stroke}" stroke-width="${cfg.strokeWidth}"${capjoin}${dash}><path d="M12 3l9 8H3l9-8z"/><path d="M6 11v9h12v-9"/></g>`;
    case "folder":
      return `<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" fill="${cfg.fill}" stroke="${cfg.stroke}" stroke-width="${cfg.strokeWidth}"${capjoin}${dash}/>`;
    case "shield":
      return cfg.fill === "currentColor"
        ? `<path d="M12 3l7 3v6c0 5-3.5 8.5-7 9c-3.5-.5-7-4-7-9V6l7-3Z" fill="${cfg.fill}"/>`
        : `<path d="M12 3l7 3v6c0 5-3.5 8.5-7 9c-3.5-.5-7-4-7-9V6l7-3Z" fill="${cfg.fill}" stroke="${cfg.stroke}" stroke-width="${cfg.strokeWidth}"${capjoin}${dash}/>`;
    case "link":
      return `<g fill="${cfg.fill}" stroke="${cfg.stroke}" stroke-width="${cfg.strokeWidth}"${capjoin}${dash}><path d="M10 14a5 5 0 0 1 0-7l1-1a5 5 0 0 1 7 7l-1 1"/><path d="M14 10a5 5 0 0 1 0 7l-1 1a5 5 0 1 1-7-7l1-1"/></g>`;
    case "circle":
    default:
      return cfg.fill === "currentColor"
        ? `<circle cx="12" cy="12" r="8" fill="${cfg.fill}"/>`
        : `<circle cx="12" cy="12" r="8" fill="${cfg.fill}" stroke="${cfg.stroke}" stroke-width="${cfg.strokeWidth}"${capjoin}${dash}/>`;
  }
}


Update package.json scripts (if needed):

{
  "scripts": {
    "dev:api": "tsx server/server.ts"
  }
}


Run:

npm i express cors
npm i -D tsx @types/express @types/cors
npm run dev:api


If your Vite dev server is on 5173 and API on 3000, add a proxy in vite.config.ts:

server: {
  proxy: {
    "/api": "http://localhost:3000"
  }
}
