Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

DanielChahine0/comicverse-api

Open more actions menu

Repository files navigation

ComicVerse API

Every comic universe, one API. One unified, typed, documented REST API over four free comic-metadata providers - Comic Vine, Metron Cloud, Marvel, and SuperHero API.

CI License: MIT Node >=20 TypeScript Made with Fastify Tests

Search comics, characters, creators, publishers, and series - or fetch a single canonical record - without caring which upstream the data came from. Every provider is normalized into one schema, every result carries per-source provenance, and a single upstream being down, rate-limited, or unconfigured never fails the whole request.


Table of contents


Why this exists

Comic metadata is scattered across several free APIs, each with its own auth scheme, rate limits, response shape, and gaps in coverage. Building anything on top of them means writing the same fan-out, normalization, caching, and failure-handling glue over and over. This project does that work once: it puts a single, key-gated, OpenAPI-documented surface in front of all four, returns one consistent schema with provenance, and degrades gracefully when an upstream is unavailable. You bring your own provider keys; the API does the aggregation.

Features

  • Unified schema - every provider response is mapped into one set of Zod-validated canonical models (Comic, Character, Creator, Publisher, Series) with inferred TypeScript types.
  • Four providers with graceful degradation - results fan out in parallel via Promise.allSettled; an upstream that is down, rate-limited, or unconfigured is reported in meta.sources, never thrown. Partial data still returns 200.
  • Cross-reference dedup - records that share a deterministic cross-reference (Metron's cv_id/gcd_id matched against Comic Vine) are collapsed into one record, with a heuristic title/issue-number/year fallback. Every contributing source is preserved in sources{}.
  • Pluggable cache - in-memory TTL + LRU by default, Redis-backed with CACHE_BACKEND=redis. Per-resource TTLs (short for search, long for detail).
  • Per-provider upstream rate limiting - a token bucket per provider honors each one's documented limits (Comic Vine per hour, Metron per minute, Marvel per day, SuperHero per minute) so a burst can never get your key banned.
  • Consumer auth + per-IP rate limit - every /v1 route requires an X-API-Key (compared in constant time); @fastify/rate-limit enforces a per-IP cap.
  • Auto-generated docs - OpenAPI 3 served at /docs (Swagger UI) and /docs/json, derived directly from the Zod route schemas.
  • Fully typed and tested - 134 tests across mappers, dedup, cache, rate limiter, auth, env validation, and route integration (upstreams mocked with undici MockAgent).

Providers

A provider is enabled only when its credentials are present in the environment; otherwise it is reported as enabled: false and skipped at query time. The capability matrix below mirrors src/providers/registry.ts.

Provider Comics Characters Creators Publishers Series Auth Notes / documented limit
Comic Vine yes yes yes yes yes api_key query param + unique User-Agent Non-commercial use only. ~200 requests / resource / hour.
Metron Cloud yes yes yes yes yes HTTP Basic (username + password) Exposes cv_id + gcd_id cross-refs; primary dedup source. ~30 / minute.
Marvel yes yes yes no yes ts + md5(ts + private + public) hash No publishers. Attribution required: "Data provided by Marvel. (c) Marvel". ~3000 / day.
SuperHero API no yes no no no access token in path Characters only (powerstats, biography, appearances).

See DISCLAIMER.md for each provider's data-use terms. You must obtain your own keys and comply with them.

Architecture

flowchart TD
    Client["Client"] -- "X-API-Key" --> Fastify["Fastify (routes + Zod validation, auth, per-IP rate limit)"]
    Fastify --> Search["SearchService"]
    Search -- "Promise.allSettled fan-out" --> Registry["ProviderRegistry (enabled + capable only)"]
    Registry --> CV["ComicVineProvider"]
    Registry --> MET["MetronProvider"]
    Registry --> MAR["MarvelProvider"]
    Registry --> SH["SuperHeroProvider"]
    CV --> Mappers["client -> mapper -> canonical model"]
    MET --> Mappers
    MAR --> Mappers
    SH --> Mappers
    Mappers --> Infra["per-provider cache + token-bucket rate limiter"]
    Infra --> Merge["merge + dedup (cv_id/gcd_id + title/number/year heuristics)"]
    Merge --> Envelope["unified envelope: results + meta.sources + warnings"]
    Envelope --> Client
Loading

Core principle: a single upstream being down, rate-limited, or unconfigured never fails the whole request. Per-provider failures are surfaced in a meta.sources block while every other provider's results still return (HTTP 200 with partial data).

Quickstart

Requires Node.js >=20.

# 1. Clone
git clone https://github.com/DanielChahine0/comicverse-api.git
cd comicverse-api

# 2. Configure - at minimum set API_KEYS (the example already ships a dev key)
cp .env.example .env

# 3. Install
npm install

# 4. Run in watch mode
npm run dev

The server boots on http://localhost:3000. It runs out of the box with no provider keys - every provider simply reports not-configured. Add provider credentials to .env to start getting real data.

Check liveness (no key required):

curl http://localhost:3000/health
{ "status": "ok", "uptime": 1.53, "version": "0.1.0" }

List providers (requires the key):

curl -H "X-API-Key: dev-key-123" http://localhost:3000/v1/providers
{
  "providers": [
    { "provider": "comicvine", "enabled": false, "capabilities": ["comics", "characters", "creators", "publishers", "series"] },
    { "provider": "metron",    "enabled": false, "capabilities": ["comics", "characters", "creators", "publishers", "series"] },
    { "provider": "marvel",    "enabled": false, "capabilities": ["comics", "characters", "creators", "series"] },
    { "provider": "superhero", "enabled": false, "capabilities": ["characters"] }
  ]
}

Search (requires the key). With no providers configured you get an empty result set plus a transparent per-source status:

curl -H "X-API-Key: dev-key-123" "http://localhost:3000/v1/search/comics?q=saga"
{
  "results": [],
  "meta": {
    "pagination": { "page": 1, "pageSize": 20, "total": 0, "hasMore": false },
    "sources": [
      { "provider": "comicvine", "queried": false, "ok": false, "skippedReason": "not-configured" },
      { "provider": "metron",    "queried": false, "ok": false, "skippedReason": "not-configured" },
      { "provider": "marvel",    "queried": false, "ok": false, "skippedReason": "not-configured" }
    ]
  }
}

Browse the interactive docs at http://localhost:3000/docs, or fetch the raw OpenAPI document from http://localhost:3000/docs/json.

Configuration

All configuration is read from the environment and validated with Zod at boot (src/config/env.ts); an invalid value fails fast. The table below documents every variable. See .env.example for a copy-paste starting point.

Variable Description Default Required
NODE_ENV Environment: development | test | production development no
PORT HTTP listen port 3000 no
HOST Bind address 0.0.0.0 no
LOG_LEVEL fatal | error | warn | info | debug | trace info no
API_KEYS Comma-separated consumer keys accepted in the X-API-Key header - yes
RATE_LIMIT_MAX Per-IP request cap for the consumer rate limiter 120 no
RATE_LIMIT_WINDOW Window for the per-IP rate limiter 1 minute no
CACHE_BACKEND Cache backend: memory | redis memory no
CACHE_TTL_SEARCH TTL (seconds) for search responses 300 no
CACHE_TTL_DETAIL TTL (seconds) for detail records 3600 no
CACHE_MAX_ITEMS Max entries in the in-memory cache before LRU eviction 1000 no
REDIS_URL Redis connection string (used when CACHE_BACKEND=redis) - no
COMICVINE_API_KEY Comic Vine API key. Setting it enables the Comic Vine provider. - no
COMICVINE_USER_AGENT Unique User-Agent Comic Vine requires on every request comicverse-api/0.1 (+https://github.com/DanielChahine0/comicverse-api) no
COMICVINE_RATE_PER_HOUR Comic Vine token-bucket limit (requests/hour) 200 no
METRON_USERNAME Metron username. Enables Metron when set together with the password. - no
METRON_PASSWORD Metron password (HTTP Basic) - no
METRON_RATE_PER_MINUTE Metron token-bucket limit (requests/minute) 30 no
MARVEL_PUBLIC_KEY Marvel public key. Enables Marvel when set together with the private key. - no
MARVEL_PRIVATE_KEY Marvel private key (used in the ts+hash signature) - no
MARVEL_RATE_PER_DAY Marvel token-bucket limit (requests/day) 3000 no
SUPERHERO_TOKEN SuperHero API token. Setting it enables the SuperHero provider. - no
HTTP_TIMEOUT_MS Per-request timeout for upstream calls (ms) 10000 no
HTTP_RETRIES Retries (with backoff) for transient upstream failures 2 no

A provider is enabled only when all of its credentials are present:

  • Comic Vine - COMICVINE_API_KEY
  • Metron - METRON_USERNAME and METRON_PASSWORD
  • Marvel - MARVEL_PUBLIC_KEY and MARVEL_PRIVATE_KEY
  • SuperHero - SUPERHERO_TOKEN

API reference

Base URL: http://localhost:3000. Every endpoint under /v1 requires authentication; /health and /docs are open.

Response envelope

List endpoints (search and series issues) return a uniform envelope:

{
  "results": [ /* canonical records */ ],
  "meta": {
    "pagination": { "page": 1, "pageSize": 20, "total": 42, "hasMore": true },
    "sources": [
      { "provider": "metron", "queried": true, "ok": true, "count": 12 },
      { "provider": "comicvine", "queried": false, "ok": false, "skippedReason": "not-configured" }
    ]
  },
  "warnings": [ /* optional human-readable notes */ ]
}

meta.sources is the transparency layer - one entry per relevant provider:

Field Meaning
provider comicvine | metron | marvel | superhero
queried whether the provider was actually called
ok whether it returned successfully
count number of records it contributed (when ok)
skippedReason why it was skipped or failed (see below)
error optional error detail string

skippedReason is one of:

Value Meaning
not-configured the provider has no credentials in the environment
not-capable the provider does not support this resource type (e.g. SuperHero for comics)
rate-limited the per-provider token bucket would have exceeded the documented limit
error the upstream call failed

Detail endpoints return a single canonical record (no envelope). Every canonical record carries a sources map keyed by provider name, recording the native id, optional URL, and any cross-reference ids (cvId, gcdId).

Authentication

Send your key in the X-API-Key header on every /v1 request. The value must match one of the comma-separated entries in API_KEYS; comparison is constant-time.

curl -H "X-API-Key: dev-key-123" "http://localhost:3000/v1/providers"

A missing or invalid key returns 401:

{ "error": { "code": "UNAUTHORIZED", "message": "Missing or invalid API key" } }

System endpoints

GET /health

Liveness probe. No auth.

curl http://localhost:3000/health
{ "status": "ok", "uptime": 1.53, "version": "0.1.0" }

GET /docs and GET /docs/json

Swagger UI and the raw OpenAPI 3 document. No auth.

GET /v1/providers

Status and capabilities of all four upstreams. Auth required.

curl -H "X-API-Key: dev-key-123" http://localhost:3000/v1/providers
{
  "providers": [
    { "provider": "comicvine", "enabled": false, "capabilities": ["comics", "characters", "creators", "publishers", "series"] },
    { "provider": "metron",    "enabled": false, "capabilities": ["comics", "characters", "creators", "publishers", "series"] },
    { "provider": "marvel",    "enabled": false, "capabilities": ["comics", "characters", "creators", "series"] },
    { "provider": "superhero", "enabled": false, "capabilities": ["characters"] }
  ]
}

Search endpoints

All search endpoints require auth and a non-empty q query parameter, and return the list envelope. Shared optional parameters:

Param Type Description
q string (required) search query
page int >= 1 page number (default 1)
pageSize int 1-100 results per page (default 20)
sources string comma-separated provider names to restrict the query, e.g. metron,comicvine

GET /v1/search

Unified multi-type search. Returns results grouped by resource type (not the flat list envelope):

curl -H "X-API-Key: dev-key-123" "http://localhost:3000/v1/search?q=spider-man"
{
  "results": {
    "comics": [ /* Comic[] */ ],
    "characters": [ /* Character[] */ ],
    "creators": [ /* Creator[] */ ],
    "publishers": [ /* Publisher[] */ ],
    "series": [ /* Series[] */ ]
  },
  "meta": { "sources": [ /* one merged SourceStatus per provider */ ] }
}

GET /v1/search/comics

Comic search. In addition to the shared params, accepts comic-specific filters:

Param Type Description
publisher string filter by publisher name
year int filter by cover/publication year
series string filter by series/volume name
creator string filter by creator name
curl -H "X-API-Key: dev-key-123" \
  "http://localhost:3000/v1/search/comics?q=saga&publisher=Image&year=2012&page=1&pageSize=20"

Representative populated result (canonical Comic, consistent with src/models/comic.ts):

{
  "results": [
    {
      "id": "metron:31415",
      "title": "Saga #1",
      "issueNumber": "1",
      "description": "When two soldiers from opposite sides of a galactic war fall in love...",
      "coverDate": { "year": 2012, "month": 3, "day": 14, "iso": "2012-03-14" },
      "publisher": "Image Comics",
      "series": { "id": "8472", "name": "Saga" },
      "creators": [
        { "name": "Brian K. Vaughan", "roles": ["writer"] },
        { "name": "Fiona Staples", "roles": ["artist", "cover"] }
      ],
      "characters": [{ "name": "Alana" }, { "name": "Marko" }],
      "images": [{ "kind": "cover", "url": "https://example.com/saga-1.jpg" }],
      "price": { "amount": 2.99, "currency": "USD" },
      "pageCount": 44,
      "sources": {
        "metron": { "provider": "metron", "id": 31415, "cvId": 360940, "url": "https://metron.cloud/issue/saga-2012-1/" },
        "comicvine": { "provider": "comicvine", "id": "4000-360940", "url": "https://comicvine.gamespot.com/saga-1/4000-360940/" }
      }
    }
  ],
  "meta": {
    "pagination": { "page": 1, "pageSize": 20, "total": 1, "hasMore": false },
    "sources": [
      { "provider": "comicvine", "queried": true, "ok": true, "count": 1 },
      { "provider": "metron", "queried": true, "ok": true, "count": 1 }
    ]
  }
}

The two raw records above (metron:31415 and the Comic Vine issue 4000-360940) were collapsed into a single canonical comic because Metron's cvId matches the Comic Vine id - both are retained under sources.

GET /v1/search/characters

curl -H "X-API-Key: dev-key-123" "http://localhost:3000/v1/search/characters?q=spider-man"

Representative populated Character (consistent with src/models/character.ts; powerstats is contributed only by SuperHero):

{
  "results": [
    {
      "id": "superhero:620",
      "name": "Spider-Man",
      "realName": "Peter Parker",
      "aliases": ["Spidey", "Web-Slinger"],
      "publisher": "Marvel Comics",
      "powerstats": { "intelligence": 90, "strength": 55, "speed": 67, "durability": 75, "power": 74, "combat": 85 },
      "firstAppearance": "Amazing Fantasy #15",
      "images": [{ "kind": "thumbnail", "url": "https://example.com/spider-man.jpg" }],
      "sources": {
        "superhero": { "provider": "superhero", "id": 620 },
        "marvel": { "provider": "marvel", "id": 1009610, "url": "https://www.marvel.com/characters/spider-man-peter-parker" }
      }
    }
  ],
  "meta": {
    "pagination": { "page": 1, "pageSize": 20, "total": 1, "hasMore": false },
    "sources": [
      { "provider": "comicvine", "queried": false, "ok": false, "skippedReason": "not-configured" },
      { "provider": "marvel", "queried": true, "ok": true, "count": 1 },
      { "provider": "superhero", "queried": true, "ok": true, "count": 1 }
    ]
  }
}

GET /v1/search/creators

Search creators / authors. Shared params only.

curl -H "X-API-Key: dev-key-123" "http://localhost:3000/v1/search/creators?q=brian%20k%20vaughan"

GET /v1/search/publishers

Search publishers. Shared params only. (Marvel and SuperHero do not contribute publishers and are reported with skippedReason: "not-capable" when explicitly requested via sources.)

curl -H "X-API-Key: dev-key-123" "http://localhost:3000/v1/search/publishers?q=image"

GET /v1/search/series

Search series / volumes. Shared params (plus publisher and year are accepted as filters).

curl -H "X-API-Key: dev-key-123" "http://localhost:3000/v1/search/series?q=saga"

Detail endpoints

Detail endpoints fetch a single canonical record from one named source. The path is /{type}/:source/:id, where :source is one of comicvine | metron | marvel | superhero and :id is that provider's native id. They require auth and return the bare record (no list envelope).

A request returns 404 when the source is unknown or not configured, when that provider does not support the resource type, or when no record matches the id.

Endpoint Description Notes
GET /v1/comics/:source/:id Canonical comic detail
GET /v1/characters/:source/:id Canonical character detail
GET /v1/creators/:source/:id Canonical creator detail
GET /v1/publishers/:source/:id Canonical publisher detail marvel and superhero always 404 (not supported)
GET /v1/series/:source/:id Canonical series / volume detail
GET /v1/series/:source/:id/issues Issues within a series / volume accepts ?page= and ?pageSize=; returns the list envelope
curl -H "X-API-Key: dev-key-123" "http://localhost:3000/v1/comics/metron/31415"
curl -H "X-API-Key: dev-key-123" "http://localhost:3000/v1/series/comicvine/8472/issues?page=1&pageSize=20"

The series-issues endpoint returns the list envelope with a single-source meta.sources:

{
  "results": [ /* Comic[] */ ],
  "meta": {
    "sources": [{ "provider": "comicvine", "queried": true, "ok": true, "count": 20 }]
  }
}

A 404 from a detail endpoint:

{ "error": { "code": "NOT_FOUND", "message": "comics '999999' not found in metron" } }

Errors

Every error response uses the same envelope: { "error": { "code", "message", "details?" } }.

Code HTTP When
VALIDATION_ERROR 400 request failed Zod validation (e.g. missing q)
UNAUTHORIZED 401 missing or invalid X-API-Key
NOT_FOUND 404 unknown source, unsupported resource, or no matching record
RATE_LIMITED 429 per-IP consumer rate limit exceeded
PROVIDER_ERROR 502 an upstream provider failed in a way that could not be degraded
INTERNAL_ERROR 500 unexpected server error

A validation failure (e.g. GET /v1/search/comics without q):

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": { "issues": [{ "path": "/q", "message": "Required" }] }
  }
}

Caching & rate limiting

Caching. Every provider call is cached under a namespaced key (provider:method:hash(params)). Search responses use a short TTL (CACHE_TTL_SEARCH, default 300s) because they are volatile; detail records use a longer TTL (CACHE_TTL_DETAIL, default 3600s) because they are stable. The default backend is an in-process TTL + LRU store bounded by CACHE_MAX_ITEMS. Set CACHE_BACKEND=redis and REDIS_URL to share the cache across instances.

Upstream rate limiting. Each provider gets its own token bucket sized to that provider's documented limit (COMICVINE_RATE_PER_HOUR, METRON_RATE_PER_MINUTE, MARVEL_RATE_PER_DAY, and a fixed 60/minute for SuperHero). When a call would exceed the bucket it fails fast and is reported with skippedReason: "rate-limited" rather than risking a key ban.

Consumer rate limiting. @fastify/rate-limit enforces a per-IP cap of RATE_LIMIT_MAX requests per RATE_LIMIT_WINDOW. Exceeding it returns 429 with the standard error envelope.

Running with Docker

The project ships a multi-stage Dockerfile and a docker-compose.yml (API plus an optional Redis service behind a Compose profile).

# Build and run the API alone (in-memory cache)
docker compose up --build

To use the Redis-backed cache, enable the redis profile and point the cache at it:

CACHE_BACKEND=redis REDIS_URL=redis://redis:6379 \
  docker compose --profile redis up --build

Configuration is supplied through the same environment variables as local development - mount or pass your .env, or set them in the Compose environment. The container exposes PORT (default 3000).

Testing

npm test          # run the full suite once (vitest run)
npm run test:watch # watch mode

The suite has 134 tests: unit tests for every provider mapper (raw fixture to canonical model), the dedup logic, both cache backends, the token-bucket rate limiter, the auth comparison, and env validation; plus integration tests that exercise every route with upstreams mocked via undici MockAgent and captured fixtures, asserting partial-failure behavior, auth gating, validation errors, and dedup output.

Other quality gates:

npm run typecheck  # tsc --noEmit
npm run lint       # eslint .
npm run format     # prettier --write .

Project structure

src/
  server.ts            # boot: read env, build app, listen
  app.ts               # buildApp(deps) -> Fastify instance (testable, no listen)
  config/
    env.ts             # Zod-validated env config, fail-fast at boot
  core/
    httpClient.ts      # undici-based: timeouts, retries + backoff, auth injection
    cache/             # Cache interface + memory and redis backends
    rateLimiter.ts     # per-provider token bucket (perHour/perMinute/perDay)
    errors.ts          # typed errors -> consistent JSON envelope
    logger.ts          # pino config
  models/              # canonical Zod schemas + inferred TS types
    common.ts comic.ts character.ts creator.ts publisher.ts series.ts
  providers/
    types.ts base.ts registry.ts
    comicvine/  metron/  marvel/  superhero/   # each: client + mapper + index
  services/
    aggregate.ts       # parallel fan-out with partial-failure handling
    dedup.ts           # cross-ref + heuristic merge
    searchService.ts   # orchestrates aggregate + dedup per resource type
  routes/
    health.ts providers.ts search.ts
    comics.ts characters.ts creators.ts publishers.ts series.ts
    shared.ts          # shared deps, Zod request schemas, detail resolver
  plugins/
    auth.ts            # X-API-Key check (constant-time), exempts /health + /docs
    rateLimit.ts       # @fastify/rate-limit (per-IP)
    swagger.ts         # @fastify/swagger + swagger-ui at /docs
    errorHandler.ts    # central error -> JSON envelope
test/
  unit/                # mappers, dedup, cache, rate limiter, auth, env
  integration/         # routes with upstreams mocked (undici MockAgent)
  fixtures/            # captured sample raw responses per provider

Roadmap

  • Additional providers (e.g. Grand Comics Database direct, League of Comic Geeks).
  • Configurable merge precedence per field.
  • Optional persistent catalog store for offline/warm-start scenarios.
  • Cursor-based pagination for large result sets.
  • Webhooks / change feeds for tracked series.

Out of scope by design: a persistent catalog DB, a web UI, user accounts beyond API-key gating, write operations to upstreams, and GraphQL.

Contributing

Contributions are welcome. See CONTRIBUTING.md for dev setup, the npm scripts, conventional-commit and branch conventions, the TDD expectation, and the pre-PR checklist. All participants are expected to follow the Code of Conduct. To report a security issue, see SECURITY.md.

License

Released under the MIT License.

Disclaimer

This project is a metadata aggregator, not a data redistributor. It bundles and caches no upstream data beyond transient operational caching, and you must obtain your own provider API keys and comply with each provider's terms - including Comic Vine's non-commercial-only restriction and Marvel's attribution requirement. Read DISCLAIMER.md before deploying.

About

Every comic universe, one API - a unified, typed, OpenAPI-documented REST API aggregating Comic Vine, Metron, Marvel, and SuperHero comic metadata.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages

Morty Proxy This is a proxified and sanitized view of the page, visit original site.