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

@media-sdk/core

v1.0.0

Published

Headless Pexels media client for photos and videos

Readme

@media-sdk/core

Headless media clients for Pexels and Pixabay. Provides typed API access, in-memory caching with request deduplication, and explicit user-action event tracking.

Installation

pnpm add @media-sdk/core

Quick Start

import {
  ApiKeyProvider,
  PexelsMediaClient,
} from "@media-sdk/core";

const auth = new ApiKeyProvider(process.env.PEXELS_API_KEY!);
const client = new PexelsMediaClient(auth);

const result = await client.searchPhotos({
  query: "nature",
  page: 1,
  perPage: 20,
});

console.log(result.items);
console.log(result.pagination);

Pixabay Provider

PixabayMediaClient implements the same MediaClient interface using the Pixabay API. Authentication uses a key= query parameter (not an Authorization header).

import {
  ApiKeyProvider,
  PixabayMediaClient,
} from "@media-sdk/core";

const auth = new ApiKeyProvider(process.env.PIXABAY_API_KEY!);
const client = new PixabayMediaClient(auth);

const photos = await client.searchPhotos({
  query: "nature",
  page: 1,
  perPage: 20,
});

const videos = await client.searchVideos({
  query: "ocean",
  page: 1,
  perPage: 15,
});

const photo = await client.getPhoto(12345);
const video = await client.getVideo(67890);

Pixabay vs Pexels

| Capability | Pexels | Pixabay | |------------|--------|---------| | Photo search | ✅ | ✅ | | Video search | ✅ | ✅ | | Get photo | ✅ | ✅ | | Get video | ✅ | ✅ | | Pagination | ✅ | ✅ | | Cancellation | ✅ | ✅ | | Events | ✅ | ✅ | | Curated photos | ✅ | ❌ |

getCuratedPhotos() on PixabayMediaClient throws a MediaError with code UNSUPPORTED_CAPABILITY. Use PexelsMediaClient for curated content.

Capabilities

Each SDK client exposes a static capabilities property describing supported operations and search filters. Use getCapabilities(client) to read capabilities from any MediaClient — custom implementations without capabilities receive an all-false fallback.

import {
  ApiKeyProvider,
  getCapabilities,
  PexelsMediaClient,
  PixabayMediaClient,
} from "@media-sdk/core";

const pexels = new PexelsMediaClient(new ApiKeyProvider(process.env.PEXELS_API_KEY!));
const pixabay = new PixabayMediaClient(new ApiKeyProvider(process.env.PIXABAY_API_KEY!));

const pexelsCaps = getCapabilities(pexels);
const pixabayCaps = getCapabilities(pixabay);

if (pexelsCaps.operations.curatedPhotos) {
  await pexels.getCuratedPhotos({ page: 1, perPage: 20 });
}

// pixabayCaps.operations.curatedPhotos === false — guard before calling

Operation capabilities

| Key | Pexels | Pixabay | | --- | --- | --- | | searchPhotos | ✅ | ✅ | | searchVideos | ✅ | ✅ | | getPhoto | ✅ | ✅ | | getVideo | ✅ | ✅ | | curatedPhotos | ✅ | ❌ | | trackView | ✅ | ✅ | | trackDownload | ✅ | ✅ |

Filter capabilities

Filters are capability-gated separately for photos and videos.

| Filter | Pexels photos | Pexels videos | Pixabay photos | Pixabay videos | | --- | --- | --- | --- | --- | | orientation | ✅ | ✅ | ✅ | ❌ | | size | ✅ | ✅ | ❌ | ❌ | | color | ✅ | ❌ | ✅ | ❌ | | category | ❌ | ❌ | ✅ | ✅ | | minWidth | ❌ | ❌ | ✅ | ✅ | | minHeight | ❌ | ❌ | ✅ | ✅ | | editorsChoice | ❌ | ❌ | ✅ | ✅ | | locale | ✅ | ✅ | ✅ | ❌ |

Check capabilities.photoFilters / capabilities.videoFilters before sending UI-selected filters. Unsupported filters throw MediaError with code: "UNSUPPORTED_FILTER" — filters are never silently ignored.

Pixabay limitations

  • Authentication — Pexels uses an Authorization header (ApiKeyProvider value is sent as the header). Pixabay uses a key= query parameter on every request.
  • Curated photosgetCuratedPhotos is Pexels-only; Pixabay has no equivalent endpoint.
  • Hotlinking — Pixabay requires serving images and videos from the URLs returned by the API (previewURL, webformatURL, largeImageURL, video rendition URLs). Do not download and re-host media on your own CDN.
  • Caching — The SDK caches API JSON responses in memory (same as Pexels). Pixabay's terms restrict how long you may cache API responses; tune MemoryCache TTL or disable caching if your usage requires stricter compliance.

Pixabay video hits are normalized into VideoFile entries for each available rendition (large, medium, small, tiny).

Client API

Both PexelsMediaClient and PixabayMediaClient implement the MediaClient interface:

  • searchPhotos(params) — search photos
  • searchVideos(params) — search videos
  • getPhoto(id) — fetch a single photo
  • getVideo(id) — fetch a single video
  • getCuratedPhotos(params?) — fetch curated photos (Pexels only)

Search methods return PaginatedResponse<T> with normalized items and pagination fields.

API methods do not emit events. They only fetch and normalize data.

Search

const photos = await client.searchPhotos({
  query: "mountains",
  page: 1,
  perPage: 20,
});

const videos = await client.searchVideos({
  query: "ocean",
  page: 2,
  perPage: 15,
});

SearchParams accepts an optional signal (AbortSignal) for request cancellation.

Search filters

Pass optional photoFilters or videoFilters on SearchParams. Each provider maps supported fields to its wire API; unsupported fields throw MediaError with code: "UNSUPPORTED_FILTER".

// Pexels photo search with orientation and color
const photos = await pexels.searchPhotos({
  query: "mountains",
  page: 1,
  perPage: 20,
  photoFilters: {
    orientation: "landscape",
    size: "large",
    color: "green",
    locale: "en-US",
  },
});

// Pixabay photo search with category and dimensions
const pixabayPhotos = await pixabay.searchPhotos({
  query: "nature",
  page: 1,
  perPage: 20,
  photoFilters: {
    orientation: "landscape",
    color: "green",        // Pixabay maps to `colors` wire param
    category: "nature",
    minWidth: 1920,
    minHeight: 1080,
    editorsChoice: true,
    locale: "en",
  },
});

// Pexels video search
const videos = await pexels.searchVideos({
  query: "ocean",
  page: 1,
  perPage: 15,
  videoFilters: {
    orientation: "landscape",
    size: "medium",
    locale: "en-US",
  },
});

Guard filters with capabilities before calling:

const caps = getCapabilities(client);

const filters: PhotoSearchFilters = {};
if (caps.photoFilters.orientation) filters.orientation = "landscape";
if (caps.photoFilters.category) filters.category = "nature";

await client.searchPhotos({ query: "nature", photoFilters: filters });

Single media

const photo = await client.getPhoto(12345);
const video = await client.getVideo(67890);

Pagination

Search and curated responses include a normalized pagination object:

const { items, pagination } = await client.searchPhotos({
  query: "nature",
  page: 1,
  perPage: 20,
});

if (pagination.hasNext) {
  const next = await client.searchPhotos({
    query: "nature",
    page: pagination.page + 1,
    perPage: pagination.perPage,
  });
}

if (pagination.hasPrevious) {
  const prev = await client.searchPhotos({
    query: "nature",
    page: pagination.page - 1,
    perPage: pagination.perPage,
  });
}

| Field | Type | Description | |-------|------|-------------| | page | number | Current page number | | perPage | number | Items per page | | totalResults | number? | Total matching results | | hasNext | boolean | Whether a next page exists | | hasPrevious | boolean | Whether a previous page exists | | nextPage | string? | Raw Pexels next-page URL | | prevPage | string? | Raw Pexels previous-page URL |

Use hasNext / hasPrevious for navigation logic. The URL fields are preserved for advanced use cases but are not required for page-based navigation.

Cache

By default, the client uses an in-memory cache with a 60-second TTL and deduplicates concurrent requests for the same key.

import {
  ApiKeyProvider,
  MemoryCache,
  PexelsMediaClient,
} from "@media-sdk/core";

const client = new PexelsMediaClient(auth, {
  cache: new MemoryCache(120_000),
});

Inject a custom Cache implementation for testing or alternate storage:

import type { Cache } from "@media-sdk/core";

const cache: Cache = {
  get: (key) => store.get(key),
  set: (key, value) => store.set(key, value),
  delete: (key) => store.delete(key),
  clear: () => store.clear(),
};

const client = new PexelsMediaClient(auth, { cache });

Cache behavior:

  • Successful responses are cached; failures are not
  • Concurrent callers for the same key share one in-flight request
  • Cache keys are namespaced (photos:, videos:, photo:, video:, curated:)

HttpClient

By default, the client uses FetchHttpClient internally. Inject a custom HttpClient for testing, proxying, or alternate transports:

import type { HttpClient } from "@media-sdk/core";
import { ApiKeyProvider, PexelsMediaClient } from "@media-sdk/core";

const httpClient: HttpClient = {
  async get(url, options) {
    const response = await fetch(url, {
      headers: options?.headers,
      signal: options?.signal,
    });
    return response.json();
  },
};

const client = new PexelsMediaClient(
  new ApiKeyProvider(process.env.PEXELS_API_KEY!),
  { httpClient },
);

FetchHttpClient is internal and not exported from the package entry.

Events

Events represent user actions, not API traffic. Call trackView() or trackDownload() explicitly from your application layer (for example when opening a preview or starting a download).

const unsubscribe = client.on("media:view", (event) => {
  console.log(event.mediaId, event.mediaType);
});

client.trackView({
  mediaId: 123,
  mediaType: "photo",
});

client.trackDownload({
  mediaId: 456,
  mediaType: "video",
});

unsubscribe();

Event types:

  • "media:view" — emitted by trackView()
  • "media:download" — emitted by trackDownload()

on() returns an unsubscribe function. Multiple listeners are supported.

Errors

| Failure | Error type | code | |---------|------------|--------| | HTTP 4xx / 5xx | MediaError with status | optional | | Unsupported method (e.g. curated on Pixabay) | MediaError | UNSUPPORTED_CAPABILITY | | Unsupported search filter | MediaError | UNSUPPORTED_FILTER | | Request cancelled via AbortSignal | AbortError / DOMException | — | | Network / fetch rejection | native Error | — | | Invalid JSON on 2xx response | native Error (parse failure) | — | | Missing API key in ApiKeyProvider | native Error | — |

import { isAbortError, MediaError } from "@media-sdk/core";

try {
  await client.searchPhotos({ query: "nature", page: 1, perPage: 20 });
} catch (error) {
  if (isAbortError(error)) {
    return; // request was cancelled
  }
  if (error instanceof MediaError) {
    if (error.code === "UNSUPPORTED_FILTER") {
      console.error("Filter not supported by this provider");
    } else if (error.code === "UNSUPPORTED_CAPABILITY") {
      console.error("Operation not supported by this provider");
    } else {
      console.error(error.status, error.message);
    }
  } else {
    console.error(error);
  }
}

Branch on error.code for control flow — do not parse error.message. Use isAbortError from @media-sdk/core as the canonical abort check.

Public API

Import from the package entry only:

import {
  ApiKeyProvider,
  getCapabilities,
  isAbortError,
  MediaError,
  MemoryCache,
  PexelsMediaClient,
  PixabayMediaClient,
  type Cache,
  type HttpClient,
  type HttpRequestOptions,
  type MediaCapabilities,
  type MediaClient,
  type MediaDownloadEvent,
  type MediaEventMap,
  type MediaFilterCapabilities,
  type MediaOperationCapabilities,
  type MediaType,
  type MediaViewEvent,
  type Photo,
  type PhotoSearchFilters,
  type Video,
  type VideoSearchFilters,
  type PaginatedResponse,
  type Pagination,
  type SearchParams,
  type PexelsMediaClientOptions,
  type PixabayMediaClientOptions,
} from "@media-sdk/core";

Pexels wire types (PexelsPhotoSearchResponse, PexelsVideo, etc.) are not exported from the package entry — use normalized Photo, Video, and PaginatedResponse<T>. See Migration 0.3 → 1.0.

EventEmitter, RequestManager, and FetchHttpClient are internal and not part of the public API.

Architecture

@media-sdk/core
├── PexelsMediaClient   ← Pexels API client
├── PixabayMediaClient  ← Pixabay API client
├── ApiKeyProvider      ← API key auth
├── MediaError          ← HTTP error type
├── MemoryCache         ← default cache implementation
├── MediaClient         ← interface
├── Domain types        ← Photo, Video, pagination, etc.
└── Event types         ← MediaViewEvent, MediaDownloadEvent
       │
       └── EventEmitter = internal

Higher-level packages build on top:

apps/web (or your app)
        │
        ├── @media-sdk/ui-react   ← presentational components
        │
        └── @media-sdk/react      ← hooks, provider
               │
               ▼
          @media-sdk/core         ← this package