DEV Community

Cover image for Building MIRROR: A Luxury AI Fashion Try-On App with Perfect Corp APIs
Steven Wallace
Steven Wallace

Posted on

Building MIRROR: A Luxury AI Fashion Try-On App with Perfect Corp APIs

Building MIRROR: A Luxury AI Fashion Try-On App

I built MIRROR for DeveloperWeek 2026, which is a luxury fashion e-commerce prototype that lets users virtually try on clothing, shoes, bags, and earrings using AI. Here's how it works and what I learned building it.

View the repo here

What Is MIRROR?

MIRROR is a full-stack web app that combines a luxury-styled product browsing experience with AI-powered virtual try-on. A user can browse products, open a try-on modal, upload a photo of themselves, see an AI-generated preview of them wearing the item, and add it to their cart. All of this can be done in the same page flow.

The app covers four product categories: clothing, shoes, bags, and earrings.

Each category has its own AI endpoint and unique payload structure.

Mirror Home
MIRROR Home page (Photo by Cottonbro Studio)

Tech Stack

  • Frontend: React, React Router, Context API, custom CSS (no UI framework)
  • Backend: Node.js + Express
  • AI: Perfect Corp S2S (Server-to-Server) APIs
  • Photo persistence: localStorage (up to 10 saved user photos)
  • Cart: localStorage with quantity aggregation

Architecture

The key architectural decision was keeping the frontend completely decoupled from Perfect Corp. The React app never calls Perfect Corp directly. All AI requests go through the Express backend.

Client (React) → POST /api/tryon → Express Server → Perfect Corp S2S API

This matters because Perfect Corp's APIs require publicly accessible URLs for both the user photo and the product reference image. The server handles saving the uploaded user photo to disk and constructing a public URL via ngrok.

Dynamic Endpoint Routing by Product Type

One of the more interesting backend problems was that Perfect Corp has different endpoints for each product category, and each one expects a different payload shape. I solved this with a clean switch-based config lookup:

function getPerfectConfig(productType) {
  switch (String(productType || "").toLowerCase()) {
    case "cloth":
      return { startUrl: "https://yce-api-01.makeupar.com/s2s/v2.0/task/cloth", ... };
    case "earrings":
      return { startUrl: "https://yce-api-01.makeupar.com/s2s/v2.0/task/2d-vto/earring", ... };
    case "shoes":
      return { startUrl: "https://yce-api-01.makeupar.com/s2s/v2.0/task/shoes", ... };
    case "bag":
      return { startUrl: "https://yce-api-01.makeupar.com/s2s/v2.0/task/bag", ... };
  }
}
Enter fullscreen mode Exit fullscreen mode

Then buildPayload() constructs the right request body for each type. Clothing needs garment_category and change_shoes. Shoes and bags need a gender field. Earrings are the most complex. They require ref_file_urls (array), source_info, and object_infos.

Item page
MIRROR Item page (Photo by Cottonbro Studio)

Task Polling Pattern

The Perfect Corp API is asynchronous. You POST to start a task, get back a task_id, then poll until task_status === success. I built a reusable poller:

async function pollPerfect({ pollBaseUrl, token, taskId, intervalMs = 2000, maxAttempts = 120 }) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const { json } = await httpRequest("GET", `${pollBaseUrl}/${taskId}`, { headers });

    const taskStatus = json?.data?.task_status || null;
    if (taskStatus === "success") return json;
    if (taskStatus === "error") throw new Error("Task failed");

    await sleep(intervalMs);
  }
  throw new Error("Max attempts exceeded while polling");
}
Enter fullscreen mode Exit fullscreen mode

This polls every 2 seconds, up to 120 attempts (4 minutes). In practice, results come back in a few seconds.

Photo Upload Flow

On the frontend, the TryOnModal component handles a few photo states:

  • New upload: file input → URL.createObjectURL() for instant preview
  • Save to My Photos: FileReader converts to base64 → saved to localStorage (max 10 photos)
  • Reuse saved photo: dropdown select from localStorage
  • When the user clicks Try It On, the selected photo's data URL gets sent to the backend as part of the POST body. The server decodes the base64, writes it to /uploads, and constructs a public URL for Perfect Corp to fetch.
function saveDataUrlToUploads(dataUrl, filenameBase, req) {
  const match = dataUrl.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$/);
  // ... parse mime + extension
  fs.writeFileSync(filepath, Buffer.from(base64, "base64"));
  return makePublicUrl(`/uploads/${filename}`, req);
}
Enter fullscreen mode Exit fullscreen mode

Data-Driven Products

Every product is defined in a single products.json file. One reusable ProductPage component handles all of them. Each product carries the metadata the try-on flow needs:

"id": 1,
    "title": "Bright Red Pumps",
    "price": 75,
    "tag": "New Season",
    "image": "/images/products/red-pumps.jpg",
    "type": "shoes",
    "gender": "female",
    "garment_category": "na",
    "change_shoes": "na",
    "sizes": [5, 6, 7, 8, 9, 10],
    "description": "Statement pumps in vivid red with crystal accents. A refined silhouette designed to elevate evening looks with effortless confidence.",
    "details": [
      "Red",
      "Crystal embellishments",
      "Buckle strap",
      "Leather sole"
    ]
Enter fullscreen mode Exit fullscreen mode

Adding a new product means adding one JSON entry. No code changes needed.

Virtual Try-on
MIRROR Virtual Try-On Modal (Photo by Donnie0102)

Cart System

The cart uses localStorage with a key of mirror_cart. Adding the same product + size combination increments quantity rather than duplicating the entry. The cart page shows a subtotal, per-item quantity, and instant removal.

What I'd Do Differently

  • Public URL handling: ngrok works great for demos but you need to remember to update PUBLIC_BASE_URL in .env every time you restart the tunnel. A deployed backend would eliminate this friction.
  • Photo storage: localStorage caps out at ~5MB, so 10 photos is a practical limit. A proper backend photo store would be the next step.
  • Polling on the client: Right now the server blocks while polling Perfect Corp. For production, I'd move to a job queue + WebSocket or SSE to push the result to the client when it's ready.

Running It Yourself

# Install
cd client && npm install
cd ../server && npm install

# Configure
echo "PERFECTCORP_BEARER_TOKEN=your_token_here" > server/.env
echo "PUBLIC_BASE_URL=https://your-ngrok-url.ngrok.app" >> server/.env

# Expose server (Perfect Corp needs a public URL)
ngrok http 5000

# Start backend + frontend
node server/index.js
cd client && npm start
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

MIRROR was a great way to explore AI-powered fashion tech in a hackathon format. The most interesting engineering challenge was juggling the different API contracts for each product category while keeping the frontend interface clean and consistent.

If you're looking to integrate Perfect Corp's virtual try-on APIs, the biggest thing to know upfront is the public URL requirement. Plan your image hosting strategy before you start as well, and you'll save yourself a lot of debugging.

The full source is on GitHub. Feel free to take a look or use it as a reference for your own try-on projects.

Top comments (0)