Back to Blog

Making 4 APIs Race Each Other (ZyMerge)

October 15, 2024

I got tired of opening 4 tabs to find stock photos. So I made them race.

The Problem

Designers search Unsplash. No good results. Try Pexels. Nothing. Pixabay. Still looking. That's 3 minutes gone for one image.

The Solution

One search. Four APIs. First results win.

async function searchEverywhere(query: string) { const results = await Promise.allSettled([ searchUnsplash(query), searchPexels(query), searchPixabay(query), searchNASA(query), // Yes, NASA. Space photos are cool. ]); // Don't let one slow API ruin everything return results .filter((r): r is PromiseFulfilledResult<Image[]> => r.status === 'fulfilled' ) .flatMap(r => r.value); }

Promise.allSettled is the hero here. One API times out? Who cares. Show the rest.

The Normalization Nightmare

Every API returns different data:

// Unsplash { urls: { regular: '...' }, user: { name: '...' } } // Pexels { src: { large: '...' }, photographer: '...' } // Pixabay { webformatURL: '...', user: '...' } // NASA { url: '...', title: '...' }

So I made them all speak the same language:

interface UnifiedImage { id: string; source: 'unsplash' | 'pexels' | 'pixabay' | 'nasa'; url: string; thumbnail: string; credit: string; }

Four adapters. One interface. Sanity preserved.

Real-Time Favorites with Convex

Wanted favorites to sync across devices instantly. Convex made this stupid simple:

export const toggleFavorite = mutation({ args: { imageId: v.string(), imageData: v.any() }, handler: async (ctx, { imageId, imageData }) => { const user = await ctx.auth.getUserIdentity(); const existing = await ctx.db .query('favorites') .filter(q => q.eq(q.field('imageId'), imageId)) .first(); if (existing) { await ctx.db.delete(existing._id); } else { await ctx.db.insert('favorites', { userId: user.subject, imageId, ...imageData }); } }, });

No REST endpoints. No manual cache invalidation. Just works.

The NASA Bonus

Added NASA's Image API as a joke. Turned out to be everyone's favorite feature. Space photos hit different.

Performance Numbers

Try it: zymerge.vercel.app

The source is on GitHub if you want to see how the sausage is made.

GitHub
LinkedIn
X