npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tmdb-kit

v1.0.0

Published

A tiny, typed, zero-runtime-dependency TMDB SDK for Node.js, Bun, Deno, and edge runtimes.

Readme

tmdb-kit

npm version CI license node coverage

A minimal, fully typed TMDB SDK with zero runtime dependencies. Built for Node.js, Bun, Deno, and edge runtimes.

tmdb-kit covers the most common TMDB workflows — movies, TV shows, search, trending, people, genres, and more — with a small, hand-crafted API designed for day-to-day use.

Features

  • Zero dependencies — nothing to audit, nothing to conflict.
  • Universal runtime — uses only fetch, Headers, URL, and AbortSignal. Works on Node.js 18+, Bun, Deno, Cloudflare Workers, Vercel Edge, and any fetch-compatible environment.
  • Strict TypeScript — full type safety with autocomplete for request options, response shapes, image sizes, and error handling.
  • ESM + CommonJS — dual-format output via tsdown, ready for any module system.
  • Typed resources — first-class methods for movies, TV, search, trending, people, genres, configuration, and images.
  • Injectable transport — swap the HTTP layer for tests, caching, or instrumentation without mocking fetch.
  • Image helpers — build CDN / proxy / signed URLs with per-request transform overrides.
  • Search type guards — narrow search.multi() results to movies, TV, or people with runtime-checked helpers.
  • 100% test coverage — comprehensive unit tests with mocked fetch; no real TMDB requests.

Install

npm install tmdb-kit

Quick Start

import { createTMDB } from 'tmdb-kit'

const tmdb = createTMDB({
  accessToken: process.env.TMDB_ACCESS_TOKEN!,
  defaultLanguage: 'en-US',
})

const popular = await tmdb.movies.popular({ page: 1 })
const movie = await tmdb.movies.details(popular.results[0]!.id, {
  appendToResponse: ['credits', 'videos'],
})

const posterUrl = tmdb.images.url(movie.poster_path, 'w500')

CommonJS:

const { createTMDB } = require('tmdb-kit')

const tmdb = createTMDB({ accessToken: process.env.TMDB_ACCESS_TOKEN })

Deno:

import { createTMDB } from 'npm:tmdb-kit'

const tmdb = createTMDB({ accessToken: Deno.env.get('TMDB_ACCESS_TOKEN')! })

Authentication

The SDK targets the TMDB v3 API (api.themoviedb.org/3). Two authentication methods are supported:

API Read Access Token (recommended) — works with both v3 and v4:

createTMDB({ accessToken: 'tmdb-read-access-token' })

Legacy v3 API key — sent as an api_key query parameter:

createTMDB({ apiKey: 'tmdb-v3-api-key' })

Exactly one of accessToken or apiKey is required.

Configuration

The factory accepts a few options beyond authentication:

import { createTMDB } from 'tmdb-kit'

const tmdb = createTMDB({
  accessToken: 'tmdb-read-access-token',

  // Defaults to https://api.themoviedb.org/3. Useful for tests or proxies.
  baseUrl: 'https://api.themoviedb.org/3',

  // Inject a fetch implementation. The SDK reads `globalThis.fetch` lazily
  // on every request, so test-time mocks (vi.spyOn, etc.) are picked up
  // automatically after the client has been constructed.
  fetch: customFetch,

  // Defaults applied to every request that supports them.
  defaultLanguage: 'en-US',
  defaultRegion: 'US',

  // Custom image host. Defaults to https://image.tmdb.org/t/p.
  imageBaseUrl: 'https://image.tmdb.org/t/p',

  // Image URL transform applied by the ImagesHelper. See "Image URLs".
  images: {
    transform: (url) => url.replace('image.tmdb.org/t/p', 'img-proxy.example/tmdb'),
  },

  // Extra headers merged into every request before authentication.
  headers: { 'X-Client': 'my-app' },
})

Injecting a custom transport

For tests, caching, or instrumentation, you can swap the entire transport layer instead of mocking fetch:

import { createTMDB, type TMDBTransport } from 'tmdb-kit'

const stub: TMDBTransport = {
  defaults: { imageBaseUrl: 'https://image.tmdb.org/t/p' },
  get: async (path, _options) => ({ results: [] }),
}

const tmdb = createTMDB({ accessToken: 'x', transport: stub })

transport.get() is called for every resource and for the request() escape hatch, so a single stub covers the whole surface area. TMDBTransport is exported as part of the public API.

Supported Resources

tmdb.movies.popular()
tmdb.movies.topRated()
tmdb.movies.nowPlaying()
tmdb.movies.upcoming()
tmdb.movies.details(550, { appendToResponse: ['credits', 'videos'] })
tmdb.movies.credits(550)
tmdb.movies.images(550)
tmdb.movies.recommendations(550)
tmdb.movies.similar(550)
tmdb.movies.videos(550)

tmdb.tv.popular()
tmdb.tv.topRated()
tmdb.tv.airingToday()
tmdb.tv.onTheAir()
tmdb.tv.details(1399)
tmdb.tv.credits(1399)
tmdb.tv.images(1399)
tmdb.tv.recommendations(1399)
tmdb.tv.similar(1399)
tmdb.tv.videos(1399)
tmdb.tv.seasonDetails(1399, 1)

tmdb.search.movies('Dune')
tmdb.search.tv('Dark')
tmdb.search.people('Sofia Coppola')
tmdb.search.multi('Tom Hardy')

tmdb.trending.all('week')
tmdb.trending.movies('day')
tmdb.trending.tv('day')
tmdb.trending.people('week')

tmdb.people.popular()
tmdb.people.details(31)

tmdb.genres.movies()
tmdb.genres.tv()

tmdb.configuration.details()
tmdb.configuration.countries()
tmdb.configuration.jobs()
tmdb.configuration.languages()
tmdb.configuration.primaryTranslations()
tmdb.configuration.timezones()

For an endpoint that is not wrapped yet:

const response = await tmdb.request<{ results: unknown[] }>('/discover/movie', {
  query: {
    sort_by: 'popularity.desc',
    with_genres: [28, 12],
  },
})

Image URLs

tmdb.images.url(path, size) builds a fully-qualified image URL. The helper returns null for nullish paths so UI code can branch naturally:

const posterUrl = tmdb.images.url(movie.poster_path, 'w500')

Routing images through a CDN or proxy

Pass a transform callback to rewrite the resolved URL. The most common use case is pointing TMDB image requests at a self-hosted proxy:

const tmdb = createTMDB({
  accessToken: 'x',
  images: {
    transform: (url) => url.replace('https://image.tmdb.org/t/p', 'https://img-proxy.example/tmdb'),
  },
})

tmdb.images.url('/abc.jpg', 'w500')
// → 'https://img-proxy.example/tmdb/w500/abc.jpg'

You can also pass a one-off transform per call — useful for adding signed query parameters for selected sizes:

tmdb.images.url('/abc.jpg', 'original', {
  transform: (url) => `${url}?signature=...`,
})

The transform is never invoked for nullish paths, so null results stay null.

buildImageUrl(path, size, options) is exported directly for callers that want to build URLs without a TMDBClient instance.

Search

search.multi() returns a union of movie / TV / person results. Filter out media types you do not want and narrow the result type with the provided type guards:

import {
  createTMDB,
  isMovieSearchResult,
  isTVSearchResult,
  isPersonSearchResult,
} from 'tmdb-kit'

const { results } = await tmdb.search.multi('Inception', {
  exclude: ['person'],
})

const movies = results.filter(isMovieSearchResult) // MovieMultiResult[]
const tvShows = results.filter(isTVSearchResult) // TVMultiResult[]
const people = results.filter(isPersonSearchResult) // PersonMultiResult[]
  • exclude performs client-side filtering — the TMDB multi-search endpoint has no server-side media_type filter. total_results reflects the unfiltered payload; the filtered count is just results.length.
  • Short-name aliases isMovie / isTV / isPerson are also exported for callers that prefer them.
  • The single-resource search methods (search.movies, search.tv, search.people) already return the narrowed result type, so the guards are only useful for search.multi() payloads.

Image Sizes

The ImageSize type is intentionally permissive — TMDB has added new sizes in the past (e.g. h632 for profile) without notice, and the SDK keeps the (string & {}) escape hatch so any future size keeps working at runtime. If you need the canonical set of documented sizes, the SDK ships them as readonly constants:

import {
  KNOWN_BACKDROP_SIZES,
  KNOWN_LOGO_SIZES,
  KNOWN_POSTER_SIZES,
  KNOWN_PROFILE_SIZES,
  KNOWN_STILL_SIZES,
  KNOWN_IMAGE_SIZES,
  isKnownBackdropSize,
  isKnownImageSize,
  // …plus one helper per type
} from 'tmdb-kit'

// Iterate to build a size picker
for (const size of KNOWN_POSTER_SIZES) {
  console.log(size) // 'w92' | 'w154' | 'w185' | 'w342' | 'w500' | 'w780' | 'original'
}

// Validate user-supplied sizes at runtime
if (!isKnownPosterSize(userInput)) {
  throw new Error(`Unknown TMDB poster size: ${userInput}`)
}

KnownBackdropSize / KnownPosterSize / KnownLogoSize / KnownProfileSize / KnownStillSize / KnownImageSize are the matching TS literal-union types for callers that want strict typing without giving up forward-compatibility.

Errors

import {
  TMDBRateLimitError,
  TMDBRequestError,
  TMDBResponseError,
} from 'tmdb-kit'

try {
  await tmdb.movies.popular()
} catch (error) {
  if (error instanceof TMDBRateLimitError) {
    // 429 — TMDB asked the caller to back off. The SDK does NOT retry
    // automatically; back off for `retryAfter` seconds (or use your own
    // policy) and try again.
    console.log('rate limited; retry in', error.retryAfter, 's')
  } else if (error instanceof TMDBResponseError) {
    console.log(error.status, error.statusCode, error.statusMessage)
  } else if (error instanceof TMDBRequestError) {
    // Thrown when a request fails before receiving a response (e.g. network
    // error, no fetch implementation found, or invalid client configuration).
    console.log('request error:', error.message)
  }
}

TMDBResponseError exposes convenience getters for the most common status codes:

| Getter | Status | | --- | --- | | isUnauthorized | 401 | | isForbidden | 403 | | isNotFound | 404 | | isRateLimit | 429 | | isServerError | 5xx |

All errors are also instances of the base class TMDBError; use that when you want a single catch-all.

Development

npm install
npm run typecheck
npm run test:coverage
npm run build

References