A RESTful API serving structured data on the Marvel Cinematic Universe — movies, TV shows, characters, and the timeline connecting them.
baseURL https://mcuapi.up.railway.app/api/v1
docs https://mcuapi.up.railway.app/docs
openapi https://mcuapi.up.railway.app/docs/openapi.json
health https://mcuapi.up.railway.app/health
Free, open, and no API key required.
- Movies & TV shows — release info, box office, cast, saga/phase, and where each title sits in the MCU timeline.
- Characters — bios, actors (including recasts), and every movie/show they appear in.
- Timeline — chronological ordering of the whole catalog, independent of release date.
- Hypermedia (HATEOAS) — every resource ships a HAL-style
_linksobject so clients can navigate the API without hardcoding URLs.
GET /api/v1/movies/1{
"id": 1,
"title": "Iron Man",
"_links": {
"self": { "href": "https://mcuapi.up.railway.app/api/v1/movies/1" },
"characters": { "href": "https://mcuapi.up.railway.app/api/v1/characters/movie/1" }
}
}List endpoints return page, limit, and collection _links (self, first, last, plus prev/next), preserving all other query params. Characters are fully navigable via GET /characters/{id}/movies and GET /characters/{id}/tvshows.
| Endpoint | Description |
|---|---|
GET /movies, GET /movies/{id} |
Movies, with studio/continuity/multiverse_designation/is_mcu filters |
GET /tvshows, GET /tvshows/{id} |
TV shows, same filters as movies |
GET /characters, GET /characters/{id} |
Characters, plus /characters/movie/{id}, /characters/tvshow/{id}, /characters/{id}/movies, /characters/{id}/tvshows |
GET /people, GET /people/{id} |
People (actors and directors) normalized out of the played_by/directed_by fields, plus /people/{id}/characters and /people/{id}/titles |
GET /post-credit-scenes, GET /post-credit-scenes/{id} |
Structured post- and mid-credits scenes, plus /post-credit-scenes/movie/{id} and /post-credit-scenes/tvshow/{id} |
GET /timeline |
Chronological ordering across continuities, independent of release date |
GET /upcoming |
Movies and TV shows whose release_date is strictly in the future, merged and sorted ascending. Titles with no announced release date are excluded. |
GET /titles |
Movies and TV shows merged into one paged, filterable collection (joined in Postgres via UNION ALL). Same filters as /movies//tvshows, plus type. Unlike /upcoming, undated titles are included and sorted last. |
GET /search?q= |
Unified typo-tolerant search across movies, TV shows, characters (name and alias), and people, ranked in Postgres by pg_trgm similarity(). ?type= narrows to one content type; each hit is the same full presented record as its own resource endpoint. |
GET /stats |
Dataset-wide counts — movies, tvshows, characters, people, titles, and distinct continuities/designations |
Tip
Full request/response schemas live in the Swagger docs, also available as a raw OpenAPI spec. An llms.txt is also published for LLM agents and tooling.
Note
Links are built from the request host by default. Set APP_URL (e.g. APP_URL=https://mcuapi.up.railway.app) to force the base URL behind a proxy.
A typed client is published as mcuapi-client — zero dependencies, ESM and CJS. Source lives in mcuapi-client/.
npm install mcuapi-clientimport { MCUAPI } from 'mcuapi-client';
const mcu = new MCUAPI();
const ironMan = await mcu.movies.get(1);
// walks every page for you by following _links.next
for await (const character of mcu.characters.all()) {
console.log(character.name, character.played_by);
}It's entirely optional — the API needs no client — but the types are derived from real production responses, so they catch things the entity definitions don't. box_office is a string (Postgres returns bigint as a string), and most fields are genuinely nullable.
mcuapi-mcp-public exposes the complete public read API to MCP clients over stdio. It is read-only, uses mcuapi-client, and never connects to PostgreSQL.
{
"mcpServers": {
"mcuapi": {
"command": "npx",
"args": ["-y", "mcuapi-mcp-public"]
}
}
}See its setup documentation for a development base-URL override.
The whole dataset is also committed to data/ and served over jsDelivr, so it stays reachable even if the API is down — and it doesn't count against the rate limit.
https://cdn.jsdelivr.net/gh/AugustoMarcelo/mcuapi@master/data/movies.json
https://cdn.jsdelivr.net/gh/AugustoMarcelo/mcuapi@master/data/tvshows.json
https://cdn.jsdelivr.net/gh/AugustoMarcelo/mcuapi@master/data/characters.json
https://cdn.jsdelivr.net/gh/AugustoMarcelo/mcuapi@master/data/people.json
https://cdn.jsdelivr.net/gh/AugustoMarcelo/mcuapi@master/data/post-credit-scenes.json
https://cdn.jsdelivr.net/gh/AugustoMarcelo/mcuapi@master/data/timeline.json
https://cdn.jsdelivr.net/gh/AugustoMarcelo/mcuapi@master/data/index.json
https://cdn.jsdelivr.net/gh/AugustoMarcelo/mcuapi@master/data/changelog.json
https://cdn.jsdelivr.net/gh/AugustoMarcelo/mcuapi@master/data/changelog.xml
Each dataset file is a plain array of the same records the API returns, _links included. index.json carries the record counts, a generated_at timestamp and a content_hash.
changelog.json retains each changed refresh, classifying records as added, removed, or changed and including field-level before/after values. changelog.xml is the same history as an RSS feed. Link fields are omitted from change detection because they are derived from the API base URL rather than dataset content.
Pin a tag instead of @master if you want a fixed dataset — @3.0.0/data/movies.json will never change. @master is refreshed weekly and cached by the CDN for up to 12 hours.
The landing page uses this mirror automatically: if a request to the live API fails, it re-resolves the same request against the mirror and shows a banner naming the mirror's generated_at date, rather than going blank.
Regenerate it with yarn snapshot. The output is byte-stable when the data hasn't moved, so a no-op run leaves the tree clean.
- Read-only. Every endpoint is a
GET; the API never accepts writes. - Rate limit. 100 requests per minute per IP. Responses carry
RateLimit-*headers, and exceeding it returns429. - Caching. Successful
/api/v1JSON responses are cached in Redis for up to 24 hours and invalidated after MCP data writes. Public responses remainCache-Control: public, max-age=3600and carry anETag; callers that already cached a response may therefore observe a write for up to one hour. Every error response isCache-Control: no-store. - Telemetry. Aggregate request counters live in Redis for 90 days. They do not write to Neon on API requests.
- Pagination. Omitted
pageandlimitresolve to1and10. Supplied values must be positive integers, andlimitmust not exceed100.
Invalid path IDs, pagination values, list fields, sort directions, boolean values, types, and unknown query parameters return 400; supplied invalid values are never coerced or ignored. IDs must be positive integers. Collection order is deterministic, with an id tie-breaker.
Every API error uses application/problem+json:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "page must be a positive integer",
"instance": "/api/v1/movies?page=0"
}Express · TypeScript · TypeORM · PostgreSQL — organized as Clean Architecture modules (movies, tvshows, characters, people, postCreditScenes, timeline, upcoming, stats) with tsyringe for dependency injection.
git clone https://github.com/AugustoMarcelo/mcuapi
cd mcuapi
yarn installCreate a .env from .env.example with your database credentials.
Development
# NODE_ENV=development in .env
yarn typeorm:dev migration:run # create tables
yarn dev:server # start on port 3333 (hot-reload)Production
# NODE_ENV=production in .env
# REDIS_URL must reference Railway Redis.
yarn typeorm migration:run # create tables
yarn build # compile to ./dist
yarn start # start on port 3333Note
NODE_ENV also tells ormconfig where to find migrations — src/ in development, dist/ in production.
Have a suggestion? Open an issue.