Integration Guide

How to Search Printables and MakerWorld for the Same Model

Printables and MakerWorld each have loyal users complaining about the other's search. Here's how to query both in one script and merge results that use completely different field names for the same stats.

Written by Alex P.

  • Printables
  • MakerWorld
  • 3D printing
  • 3D model search
  • STL search

A 219-upvote post in r/prusa3d — “Tired of searching Printables, then Thingiverse, then MakerWorld separately? I made a tool for that” — is a good measure of how real this problem is. The two biggest model repositories each have a devoted following complaining about the other’s search: “printables search function sucks so bad,” “MakerWorld feed is slop and search is also bad.” Ask an AI assistant how to search both at once and it points you at a handful of third-party aggregator websites — not a way to build the search yourself.

That’s the actual gap: nobody publishes a script that queries both official APIs and merges the results, because the two return their popularity stats under completely different field names. This builds that.


Query both with the same input

const API_KEY = process.env.FETCHLAYER_API_KEY;

async function searchPrintables(query) {
  const res = await fetch('https://api.fetchlayer.dev/printables/search-models', {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, sort: 'popular', limit: 20 }),
  });
  return (await res.json()).models ?? [];
}

async function searchMakerworld(query) {
  const res = await fetch('https://api.fetchlayer.dev/makerworld/search-models', {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, modelType: '3d', sortBy: 'trending', limit: 20 }),
  });
  return (await res.json()).models ?? [];
}

The sort/sortBy vocabularies don’t line up either — Printables offers best_match, latest, popular, rating, makes_count; MakerWorld offers relevance, trending, newest, boosts, downloads, likes, collections, prints. There’s no exact equivalent of “trending” on Printables or “popular” on MakerWorld — pick the closest available sort per platform rather than assuming a shared vocabulary, and note the choice differs when you compare rankings across the two.


The part that actually needs work: normalizing stats

This is the real reason nobody’s shipped this as a simple two-line combine. Printables nests its numbers under stats (downloads, likes, views, makes, comments, rating); MakerWorld uses entirely different, flatter names (downloadCount, likeCount, printCount, commentCount, boostCount) with no direct view-count or star-rating equivalent at all:

function normalize(source, model) {
  if (source === 'printables') {
    return {
      source,
      id: model.id,
      name: model.name,
      url: model.url,
      designer: model.designer?.displayName ?? model.designer?.handle,
      downloads: model.stats?.downloads ?? 0,
      likes: model.stats?.likes ?? 0,
      rating: model.stats?.ratingAverage ?? null,   // MakerWorld has no equivalent
      isPrintable: true,                            // Printables models are always printable files
      free: model.price == null,
    };
  }

  return {
    source: 'makerworld',
    id: model.modelId,
    name: model.title,
    url: model.modelUrl,
    designer: model.designer?.name ?? model.designer?.handle,
    downloads: model.stats?.downloadCount ?? 0,
    likes: model.stats?.likeCount ?? 0,
    rating: null,                                   // Printables has no equivalent (boostCount instead)
    isPrintable: model.isPrintable,                  // not every MakerWorld model ships a ready-to-print profile
    free: !model.isExclusive,
  };
}

async function searchBoth(query) {
  const [printables, makerworld] = await Promise.all([searchPrintables(query), searchMakerworld(query)]);
  return [
    ...printables.map((m) => normalize('printables', m)),
    ...makerworld.map((m) => normalize('makerworld', m)),
  ].sort((a, b) => b.downloads - a.downloads);
}

Sorting the merged list by downloads is the fairest cross-platform comparison available — it’s the one stat both platforms publish under a comparable definition, unlike likes (a lighter-weight action on both, but not necessarily calibrated the same) or rating (Printables-only).


Filtering to what you can actually print

Both platforms let you filter before you search, not after — worth doing server-side rather than in your merge step, since it’s cheaper and each API’s filter values come from its own reference endpoint:

// Printables: filter ids come from /filter-values (printers, licenses, modeling apps)
async function searchPrintablesForPrinter(query, printerId) {
  const res = await fetch('https://api.fetchlayer.dev/printables/search-models', {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, printerIds: [printerId], fileKinds: ['GCODE'], limit: 20 }),
  });
  return (await res.json()).models;
}

// MakerWorld: printableOnly skips concept-only uploads with no slice profile
async function searchMakerworldPrintable(query) {
  const res = await fetch('https://api.fetchlayer.dev/makerworld/search-models', {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, printableOnly: true, limit: 20 }),
  });
  return (await res.json()).models;
}

printerIds on Printables is a real filter against 639 known printer ids — not a free-text field — so resolve the id once via /filter-values and cache it rather than looking it up on every search.


Deduplicating the same design across both

Designers frequently upload the same model to both platforms. There’s no shared identifier to key on, so a practical (imperfect) dedupe is a normalized-name match combined with a designer-handle match — flag likely duplicates for a human to confirm rather than silently merging them:

function flagLikelyDuplicates(merged) {
  const byNormalizedName = new Map();
  for (const model of merged) {
    const key = model.name.toLowerCase().replace(/[^a-z0-9]/g, '');
    if (!byNormalizedName.has(key)) byNormalizedName.set(key, []);
    byNormalizedName.get(key).push(model);
  }
  return [...byNormalizedName.values()].filter((group) => group.length > 1);
}

Practical notes

  • isExclusive (MakerWorld) and clubExclusive (Printables) both mean “behind a paid membership,” under different names. Filter these out early if you’re building a free-model-only feed — neither platform excludes them from search results by default.
  • Printables’ firstPublishedAt can predate publishedAt. A model with a recent publishedAt isn’t necessarily new — check firstPublishedAt before flagging it as a fresh upload in a “new models this week” feed.
  • Neither platform’s search API returns a license-compatibility check. licenseIds (Printables) and licenses (MakerWorld, Creative Commons codes) filter search, but confirming a specific model’s license is compatible with a commercial use still means reading the field on the result, not assuming the filter guaranteed it.

Next Steps