SWAPI β the Star Wars API β is probably the most-tutorialized "practice fetch()" API ever made, and the original domain was swapi.co. That domain no longer serves an API at all: every swapi.co URL now 301-redirects to an integration-marketing page on pipedream.com (we re-checked while writing this; our status page for it re-checks every 30 minutes). Follow the redirect and you get HTML, so a decade of tutorial code dies in the same confusing way:
$ curl -sL https://swapi.co/api/people/1/
<!doctype html>β¦ // a marketing page, not JSON
// browser
Uncaught SyntaxError: Unexpected token '<' β¦ is not valid JSON // res.json() on HTML
Why this guide's dataset isn't Star Wars data: SWAPI's film/character data derives from someone else's franchise, which is part of why the hosting story has always been donation-run mirrors rather than a durable service. We don't rehost any of it β the starter dataset below is an original space-opera universe we wrote (ten planets, sixteen characters, ten starships) in the same field style, so the code patterns from SWAPI tutorials run unchanged while the data is ours to give you.
You have two honest options, fastest first.
| Mirror | Status | Notes |
|---|---|---|
| swapi.dev | β alive today (live status) | The best-known successor and shape-identical to old swapi.co: {count, next, previous, results} envelope, ?search=, URL-based relations. Community-run with a history of certificate-expiry outages β one in April 2025 broke Angular's own docs example. Read-only: POST /api/people/ returns 403. |
| swapi.info | β alive today | Static mirror β fast and hard to kill, same field shape as swapi.dev, but list endpoints return the entire collection as a bare array: no pagination, no ?search=, and writes 404 (there's no server). |
| swapi.tech | β alive today | Actively maintained, but a different API: everything is wrapped in {message, result}, single records hide fields under result.properties, and list endpoints return only {uid, name, url} stubs β showing a table of characters with their details means one extra fetch per row. |
If all you need is canonical Star Wars data for a read-only demo, use a mirror β swapi.dev if you want the classic shape, swapi.info if you want the thing least likely to be down mid-workshop. The rest of this guide is for when you hit what the mirrors can't do: writes that persist, numbers that are numbers, relations without N+1 fetches, your own schema β or an API that can't disappear from under your course material the way swapi.co did.
We host a starter dataset β an original universe in the SWAPI field style: planets (climate, terrain, population, diameterβ¦), characters (species, height, mass, birth_year, gender, homeworld as planet_id), starships (model, class, crew, hyperdrive_rating, pilot as character_id). Pipe it into the importer:
curl -s https://mockbird.mockbird.workers.dev/data/galaxy.db.json \
| curl -s -X POST 'https://mockbird.mockbird.workers.dev/api/projects/import?name=galaxy' \
--data-binary @-
The response contains your project id and an adminKey (save it β it manages the project, and lets you claim it into an account later). Your API is live immediately, no signup, no key, CORS on:
https://mockbird.mockbird.workers.dev/m/YOUR_ID/characters
| SWAPI | Your Mockbird API |
|---|---|
/api/people/1/ | /characters/1 |
/api/people/?search=an | /characters?name_like=an (name-only substring, same semantics) β or ?q= to search every field at once |
/api/people/?page=2 | /characters?page=2&limit=10 β total in the x-total-count header instead of a count field |
"homeworld": "https://β¦/planets/1/" β second fetch | /characters/1?_expand=planet β the full planet object arrives joined, one request |
"residents": [url, url, β¦] β N fetches | /planets/1/characters (nested route) or /planets/1?_embed=characters |
"height": "172" (string), "unknown" | "height": 172 β numbers are numbers, so ?height_gte=180 and ?sortBy=mass&order=desc just work |
| no random endpoint | /characters?sortBy=random&limit=1 |
| films/species/vehicles resources | add your own: POST /api/projects/YOUR_ID/resources or paste records in the dashboard |
| read-only | full CRUD β POST/PUT/PATCH/DELETE persist |
The classic tutorial loop, minus the string-parsing and the second fetch:
const res = await fetch(BASE + "/characters?sortBy=random&limit=1&_expand=planet");
const [who] = await res.json();
title.textContent = who.name; // "Szel of the Nine Winds"
home.textContent = who.planet.name; // "Oskarion"
mass.textContent = who.mass + " kg"; // 66 β already a number, no Number() dance
Every SWAPI tutorial ends where the interesting part begins, because the API is read-only. Yours isn't β so the natural extension of the tutorial project is letting users add to the universe:
curl -X POST "https://mockbird.mockbird.workers.dev/m/YOUR_ID/characters" \
-H 'content-type: application/json' \
-d '{"name":"Rix Amberlane","species":"human","height":181,"mass":79,
"birth_year":3199,"gender":"male","planet_id":8}'
The record persists β it shows up in GET /characters?planet_id=8, in /planets/8/characters, in the random endpoint, and in GraphQL ({ characters { name planet { name } } } β the same relations, typed). You also get delay/error simulation for teaching loading states, generated TypeScript types, and a db.json export so you can leave with your universe anytime.
| swapi.co | swapi.dev | swapi.info | swapi.tech | Your Mockbird project | |
|---|---|---|---|---|---|
| Works today | β redirects to HTML | β (outage history) | β | β | β (~60s setup) |
| Canonical Star Wars data | β | β | β | β | β original universe, deliberately |
| Classic swapi.co shape | β | β | partly (no pagination/search) | β new envelope + stub lists | β new base URL, same patterns |
| Numbers as numbers | β | β strings + "unknown" | β strings | β strings | β typed |
| Relations | β | URLs β N+1 fetches | URLs β N+1 | URLs β N+1 | β joined server-side (_expand/_embed) |
| Writes | β | β 403 | β 404 | β | β full CRUD |
| Your own schema | β | β | β | β | β |
| Key required | β | no | no | no | no, bounded free limits (10k req/day) |
The mirrors are volunteer-run gifts to the community and deserve the traffic for what they're for: canonical read-only data. Host your own when the tutorial needs to go somewhere β writes, your schema, typed fields β or when you're teaching a workshop and can't afford a surprise outage.
The one-liner in section 2 is the whole setup β or open the dashboard and paste the dataset into the import panel. swapi.co has a lot of company in the beginner-API graveyard β jservice.io times out, the Bored API's DNS is gone, numbersapi 404s (the live status board tracks 71 of these). Same fix works for all of them: we keep ready-made quotes, countries, NBA, and trivia datasets too.