Skip to main content

Migrating from the RAWG API to GameBrain

This is a field-by-field guide for moving an app from the RAWG API to GameBrain. It assumes you already have a working RAWG integration and want to change as little code as possible. Budget about half a day for a typical app; most of that is re-matching the games you've already stored, not writing new code.

If you haven't decided yet, read the comparison of RAWG alternatives first — IGDB is the better choice for some projects and we say so there.

Before you start: how usage is counted

Create a free account to get a key — it's issued immediately, no card, no sales call. Then get familiar with the meter, because it works differently from RAWG's request counter.

GameBrain meters in tokens, not requests, and allowances are daily rather than monthly. Most calls cost one token. A paginated call costs one token per started block of ten results returned, so a 10-result page costs 1 and a full 50-result page costs 5 — the same as fetching five pages of ten, but in one round trip; a wide query that comes back with three games costs 1. Autocomplete (/v1/games/suggestions) costs 0.1, so it's safe to call on keystrokes. The free plan is 50 tokens/day for non-commercial use with a link back to us; paid plans start at 500/day.

Two habits make that budget go a long way: cache aggressively (the API terms allow caching for up to 24 hours, and you may store our game ids permanently so you never re-resolve the same title twice), and use the cheap autocomplete endpoint for type-ahead instead of full search. Watch X-API-Quota-Left on every response.

Step 1: authentication

RAWG takes ?key=. GameBrain takes ?api-key=, or an x-api-key header, which is the better habit — it keeps your key out of logs and referrer headers.

// before
fetch(`https://api.rawg.io/api/games?key=${KEY}&search=hades`)

// after
fetch('https://api.gamebrain.co/v1/games?query=hades', {
  headers: { 'x-api-key': KEY }
})

Step 2: endpoints

What you're doing RAWG GameBrain
Search games GET /api/games?search= GET /v1/games?query=
Game detail GET /api/games/{id} GET /v1/games/{id}
Similar / suggested games GET /api/games/{id}/suggested GET /v1/games/{id}/similar
Screenshots GET /api/games/{id}/screenshots included in game detail as screenshots
Autocomplete (none — people used search) GET /v1/games/suggestions?query=
Find a game you already know (none — people searched by name) GET /v1/games/match?steam_id=&igdb_id=&name=&year=
Review sentiment by aspect (none) GET /v1/games/review-box?steam_id=
Per-game news (none) GET /v1/games/{id}/news

Step 3: pagination

RAWG pages with page and page_size and hands you next/previous URLs. GameBrain uses offset and limit, and tells you the total:

// RAWG: { count, next, previous, results: [...] }
// GameBrain: { query, total_results, limit, offset, results: [...] }

const limit = pageSize;              // up to 50
const offset = (page - 1) * pageSize;
const hasNext = offset + limit < body.total_results;

Two limits to design around: limit tops out at 50, and offset is capped, so you cannot page indefinitely through a large result set. If you're walking deep into results, narrow with filters instead of paging — and if you're trying to mirror the whole catalog, talk to us rather than paginating for a week.

Step 4: fields

RAWG GameBrain Note
id id Different id space. See step 5 — this is the only part that needs real work.
slug link Full URL, e.g. https://gamebrain.co/game/hades
name name
released release_date Both YYYY-MM-DD; search results also carry year
background_image image
short_screenshots[].image screenshots[] Plain array of URLs, not objects
rating (0–5) rating.mean (0–1) Multiply by 5 to keep your existing star widget
ratings_count rating.count Also count_players / count_critics
metacritic (0–100) rating.mean_critics (0–1) Multiply by 100. Aggregated from critic reviews, not Metacritic itself
platforms[].platform.name platforms[].name One level shallower; value is the stable slug
genres[].name genres[].name Same shape; genre is a ready-made headline string
tags[].name tags[].name
developers[].name developer A single string, not an array
playtime (hours) playtime.median Plus mean, min/max and percentiles
stores[].store.name offers[].store_name Each offer also carries price.value, price.initial, price.discount_percent and price.currency
description_raw description short_description is a one-paragraph summary
esrb_rating adult_only Boolean rather than a rating body's label

What doesn't come across. There's no direct equivalent for RAWG's publishers array, website, parent_platforms, the ESRB body/label detail, or the ratings breakdown by bucket. Check whether your UI actually renders those before you start — in most apps two of them turn out to be unused.

Worth wiring up while you're in there, with no RAWG equivalent: videos and gameplay (trailer/gameplay embeds), themes, play_modes, and the aspect-level review sentiment from /v1/games/review-box if your games are on Steam.

Step 5: re-matching the games you've already stored

This is the part nobody can automate away: a RAWG game id means nothing here, so any ids in your database have to be resolved to GameBrain ids once. The /v1/games/match endpoint exists for this. Match on the strongest identifier you hold, in this order:

  • Steam app id?steam_id=1145360. Exact, use it whenever you have it.
  • IGDB id?igdb_id=. Also exact.
  • Name and year?name=Hades&year=2020. Fuzzy, so spot-check the results; the year matters a lot for remasters and re-releases.

If you only stored RAWG slugs, turn the slug back into a name first ("hades-ii" → "Hades II") and match on name plus year. Run this as a one-off backfill script, log every miss, and review that list by hand — misses cluster around DLC, bundles and regional editions, and they're usually a small fraction of a library.

async function toGameBrainId(row) {
  const params = row.steamAppId
    ? `steam_id=${row.steamAppId}`
    : `name=${encodeURIComponent(row.name)}&year=${row.year}`;
  const res = await fetch(`https://api.gamebrain.co/v1/games/match?${params}`, {
    headers: { 'x-api-key': KEY }
  });
  if (res.status === 404) return null;   // log it, match by hand later
  return (await res.json()).id;
}

Run it sequentially with a small delay: the free plan allows 60 requests a minute and one concurrent request, so firing fifty promises at once will just earn you a wall of 429s.

Optional: a drop-in adapter

If RAWG's response shape is spread across your codebase, the smallest change is one adapter that returns RAWG-shaped objects, rather than editing every component. Something like this is usually enough:

const BASE = 'https://api.gamebrain.co/v1';
const headers = { 'x-api-key': process.env.GAMEBRAIN_KEY };

const toRawgShape = (g) => ({
  id: g.id,
  name: g.name,
  released: g.release_date ?? (g.year ? `${g.year}-01-01` : null),
  background_image: g.image,
  rating: g.rating?.mean != null ? +(g.rating.mean * 5).toFixed(2) : null,
  ratings_count: g.rating?.count ?? 0,
  metacritic: g.rating?.mean_critics != null
    ? Math.round(g.rating.mean_critics * 100) : null,
  playtime: g.playtime?.median ?? 0,
  platforms: (g.platforms ?? []).map((p) => ({ platform: { name: p.name, slug: p.value } })),
  genres: (g.genres ?? []).map((x) => ({ name: x.name, slug: x.value })),
  short_screenshots: (g.screenshots ?? []).map((image, id) => ({ id, image }))
});

export async function searchGames(search, page = 1, pageSize = 20) {
  const offset = (page - 1) * pageSize;
  const url = `${BASE}/games?query=${encodeURIComponent(search)}&offset=${offset}&limit=${pageSize}`;
  const body = await fetch(url, { headers }).then((r) => r.json());
  return {
    count: body.total_results,
    next: offset + pageSize < body.total_results ? page + 1 : null,
    previous: page > 1 ? page - 1 : null,
    results: body.results.map(toRawgShape)
  };
}

export async function getGame(id) {
  const g = await fetch(`${BASE}/games/${id}`, { headers }).then((r) => r.json());
  return { ...toRawgShape(g), description_raw: g.description, offers: g.offers };
}

Keep it as one module. The point isn't only this migration: the next time any provider has a bad week, you change one file.

Error codes you'll meet

  • 401 — missing or unknown API key. Check you're sending x-api-key, not RAWG's key.
  • 402 — the day's tokens are gone. This is the one RAWG users don't expect; it clears at the next daily reset. Fall back to your cache rather than showing an error.
  • 404 — no game matched. Expected on /v1/games/match; log it for manual review instead of retrying.
  • 429 — too many requests per minute, or too many at once. Back off and retry; don't hammer.

Every response carries X-API-Quota-Used and X-API-Quota-Left. Logging the latter costs nothing and turns "the app broke on a Tuesday" into a graph you can see coming.

Things that will bite you

  • Ratings are 0–1, not 0–5. The most common porting bug: every game looks like it scored 0.9 stars.
  • Autocomplete has its own endpoint. Use /v1/games/suggestions for type-ahead at 0.1 tokens; don't run full search on every keystroke at 1 token a go.
  • Cache for a day, store ids forever. The API terms allow caching responses for up to 24 hours and keeping our identifiers indefinitely, which together are the biggest lever you have on token usage: resolve a game once, store its id, and serve it from your own cache for the rest of the day.
  • Prices move, metadata doesn't. Cache game metadata for the full 24 hours, but keep offers and discounts much fresher — a wrong price is worse than a slow page.

Getting help

The full API documentation has every parameter and a live example per endpoint, and the comparison page covers the alternatives if you're still deciding. If something in your migration doesn't map cleanly, mail [email protected] — it reaches the two people who build GameBrain, and "the field I need doesn't exist" is useful feedback rather than a nuisance. There's also a Discord if you'd rather ask in the open.