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 sendingx-api-key, not RAWG'skey.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/suggestionsfor 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.