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
- 4 APIs searched in parallel: ~400ms average
- Slowest API doesn't block results
- Favorites sync: instant
Try it: zymerge.vercel.app
The source is on GitHub if you want to see how the sausage is made.