DEV Community

Anuhya Yadugiri
Anuhya Yadugiri

Posted on

BrandBridge: Designing the Backend for Marketing Content System

Designing Brandbridge for a Backend That Didn't Exist Yet

We're a team of five building Brandbridge, a marketplace that connects content creators, photographers, brands and startups, and the frontend was mine to own from the start. I had a teammate lined up to build the backend, a UI the rest of the team needed working, and no agreement yet on what the API would actually look like. That's a normal situation on a small team moving fast, but it's the kind of normal that quietly wastes a lot of time if you don't design around it deliberately. This is the story of how I structured the React frontend so the backend could show up late without forcing a rewrite, and why I think the approach paid off for the whole team, not just for me.

Why this problem is worth solving

The creator economy has a coordination problem hiding under all the hype about influencer marketing. A creator who wants a professional shoot has to go find a photographer somewhere else, on a different platform, with no shared history or reputation. A brand that wants to run a campaign has to go find creators somewhere else again. A startup trying to decide whether a campaign is even worth the spend has no easy way to see what a given niche costs to reach or how it's trending, so that decision gets made on gut feel.

Each of those is solved today by a separate, single-purpose tool: a freelance marketplace for booking photographers, an influencer platform for brand deals, a research tool for market sizing. Brandbridge's premise is that these aren't actually separate problems — they're the same person, at different points in the same workflow, and a creator who can book a photographer, apply to a campaign, and see what similar campaigns paid without leaving one platform has a meaningfully easier time than someone juggling three logins. That's the bet the product is making, and it's the reason the frontend needed to support four different user roles cleanly from day one rather than bolting them on later.

What the system does

Brandbridge has four kinds of users on one platform, which is really the whole premise of the project:

  • Creators build a portfolio, hire photographers for shoots, and apply to brand campaigns.
  • Photographers list services and rates and get booked.
  • Brands post campaigns and review who applies.
  • Startups look at market data — demand, pricing, engagement by platform — before they spend money on a campaign.

On top of that sits a fifth piece I added later: an AI content pipeline. A team submits a brief, the system checks what's worked before for that channel, drafts content, routes it through human review, publishes it, checks how it performed, and either saves the result as a pattern to reuse or feeds a "here's what to try differently" note back into the next draft. It's a loop, not a pipeline that runs once and stops — which matters, because the whole value of the feature compounds over time as it accumulates memory, rather than staying flat the way a one-shot content generator does.

The whole thing is a single Vite + React app: React Router for the six main pages, Tailwind for styling, Recharts for the one chart that needed to exist (engagement by platform, on the startup insights page), and no state management library, because nothing here needed one.

The core decision: one file is the API

The interesting engineering decision wasn't a component or a library choice. It was where I drew the line between "frontend logic" and "backend logic" when there was no backend yet, and it's the decision that makes the rest of this project credible as more than a UI mockup.

The naive approach is to hardcode sample data into your components and clean it up later. I've done that before, and "later" always costs more than it looks like it will, because by the time the backend is ready, sample data has leaked into a dozen places — a .filter() here, a hardcoded price format there — and untangling it means touching every page again.

Instead, every piece of data in the app flows through one file, src/services/api.js, and no page imports the raw sample data directly. Every function in that file already returns a Promise, even though right now it's just resolving with an array after an artificial delay:

const API_BASE = import.meta.env.VITE_API_URL || null;
const delay = (ms = 300) => new Promise((resolve) => setTimeout(resolve, ms));

export async function getTalent({ query = "", role = "all" } = {}) {
  if (API_BASE) {
    const params = new URLSearchParams({ query, role });
    const res = await fetch(`${API_BASE}/talent?${params}`);
    if (!res.ok) throw new Error(`getTalent failed: ${res.status}`);
    return res.json();
  }
  await delay();
  const q = query.toLowerCase();
  return TALENT.filter(
    (t) =>
      (role === "all" || t.role === role) &&
      `${t.name} ${t.niche} ${t.city}`.toLowerCase().includes(q)
  );
}
Enter fullscreen mode Exit fullscreen mode

The if (API_BASE) branch is the entire integration surface. Set VITE_API_URL in a .env file, and every function in that module starts making real HTTP calls instead of resolving mock arrays — no changes anywhere else in the app. My teammate doesn't need to read a single React component to know what to build; the function signatures and the fetch calls inside them are the contract. I wrote a table in the README mapping each function to a method and a path, and that became the actual spec we worked from, instead of a separate design doc that would drift out of sync with the code.

This matters beyond convenience. It means Brandbridge today is not a static prototype of an idea — it's a real, working four-sided marketplace UI with every interaction (search, filter, book, apply, sign up, view a chart) already implemented and already handling loading and empty states. The only thing missing is a database behind it, and the contract for that database is already written down.

Where it paid off: the content pipeline

The clearest test of this pattern — and the clearest sign of where the product can go next — was the AI content pipeline, because it's not a simple CRUD list. It's a state machine with a loop in it, and it's the piece of Brandbridge that turns the platform from "a directory with a booking button" into something that gets more useful the more it's used.

Content moves through: input, memory check, draft, review, and then branches. Approved goes to publish; rejected goes back to another draft. After publishing, a performance check branches again: did well goes to save-to-memory, which feeds back into future memory checks; didn't do well goes to an improve-strategy step that also loops back to review.

I modeled this as seven small async functions in src/services/contentPipeline.js, each one a discrete step in the diagram rather than one big "runPipeline()" function:

export async function checkPerformance(content) {
  if (API_BASE) {
    const res = await fetch(`${API_BASE}/pipeline/performance/${content.id}`);
    if (!res.ok) throw new Error(`checkPerformance failed: ${res.status}`);
    return res.json();
  }
  await delay(700);
  const score = Math.round(50 + Math.random() * 50);
  return { score, didWell: score >= 70 };
}
Enter fullscreen mode Exit fullscreen mode

The React component (ContentStudio.jsx) doesn't know or care that checkPerformance is currently a random number generator. It calls the function, gets back { score, didWell }, and renders based on that shape. When a real analytics query replaces the random number, the component's branching logic — "if didWell, show the save-to-memory button; if not, show the improve-strategy button" — doesn't change at all. The loop itself lives in the page's stage state, not inside the API layer, which is exactly where I wanted that logic to sit: visible, in one component, not smeared across service functions.

For a startup user of Brandbridge, this is the feature that actually answers the question the "market insights" page can only gesture at: not just what a niche costs to reach, but which specific pitch, hook, or format worked last time someone tried. That's a harder thing to build than a static dashboard, and it's the reason I think this part of the product is worth the extra design effort now rather than later.

Results, concretely

Right now, if you run the app with no VITE_API_URL set, here's a real interaction: submit a brief like "Announce our new photographer booking feature" for the Social Media channel. The UI shows "Checking memory for prior Social Media content…", then surfaces two prior entries with what worked and their scores, then produces a draft that explicitly references one of those results in its text. Reject it with a note, and version 2 comes back with the note appended, still on the review stage. Approve it, pick channels, publish, and the performance check returns a score between 50 and 100 — below 70 routes you to an "improve strategy" note and back to review; 70 or above lets you save the result, which appears immediately in an "AI memory" list underneath.

None of that required a backend to exist. And none of it will need to change when a backend does exist — only the seven function bodies in contentPipeline.js will.

What this means for what comes next

The reason I'm confident recommending we keep building on this foundation rather than restarting it once the backend arrives: the integration risk that normally shows up at the worst possible time — two weeks before launch, when frontend and backend assumptions turn out to have quietly diverged — has already been designed out. The contract is written, it's enforced by the code structure rather than by discipline alone, and every page already behaves the way it will once real data is flowing through it.

Push every piece of sample data through one file, even the small stuff. The instinct to skip this for "just one quick list" is exactly what erodes the pattern. If a page is even touching data that might one day come from a server, it goes through the service layer, no exceptions.

Make every mock function async, from day one. A synchronous mock function that returns TALENT.filter(...) directly hides a category of bugs — loading states, error states, race conditions from fast typing in a search box — that only show up once real network latency exists. Wrapping it in a Promise and an artificial delay() forces you to build the loading and error UI while it's cheap to get wrong, instead of after a real API is already plugged in.

Model branching logic as small named functions that match the diagram, not one big orchestrator. Seven small functions, each with an obvious name and single responsibility, meant the review/publish/performance branches could each be swapped for a real implementation independently, at different times, without touching the others.

Write the API contract as a table in the README, generated from the actual function signatures, not as a separate spec. Docs that live next to the code they describe are the only docs that reliably stay true. My table listing function name, HTTP method, and path took ten minutes and was more useful to my teammate than a design doc would have been, because it couldn't drift from what the code actually did.

A loading spinner in front of a hardcoded array is not the same as a loading spinner in front of a real request, but it's close enough to catch most of your mistakes early. The gap between mock and real should be as small and as localized as you can make it. Every bit of that gap I closed before the backend showed up is a bit of integration pain the project won't have to pay for later — and it's the reason I think Brandbridge is further along than a frontend built without this discipline would be at the same point.

Top comments (0)