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

@get-air/simkl

v0.1.0

Published

Promise and Effect APIs for the fully typed Simkl API client

Readme

@get-air/simkl

CI npm

A fully typed TypeScript client for the complete Simkl API. The default API uses ordinary Promises and JavaScript values. Effect applications get the same implementation, with typed errors and Layers, from @get-air/simkl/effect.

  • All 49 operations from Simkl's OpenAPI 3.1 document, grouped by domain.
  • Fully inferred path, query, header, body, and response types.
  • OAuth 2.0, PKCE, and PIN/device authentication flows.
  • Automatic app identity and optional bearer-token injection.
  • Automatic routing between api.simkl.com, simkl.com, and the data.simkl.in CDN.
  • Tagged errors for Promise users and typed Effect errors for Effect users.
  • Pagination and rate-limit response metadata.
  • Manual redirect preservation for /redirect and image redirect endpoints.
  • Simkl poster, fanart, episode-image, and fallback URL helpers.
  • Direct object-oriented construction and first-class Tauri v2 transport/caching adapters.

Install

pnpm add @get-air/simkl

Promise API

The root import does not require callers to import or understand Effect:

import { Simkl, isSimklError } from "@get-air/simkl"

const simkl = await Simkl.make({
  clientId: "your-client-id",
  appName: "my-app",
  appVersion: "1.0.0",
  accessToken: "the-user-access-token",
})

try {
  const movie = await simkl.movies.details({
    params: { path: { id: "5392" } },
  })
  console.log(movie.data.title, movie.rateLimit.remaining)
} catch (error) {
  if (isSimklError(error)) {
    console.error(error._tag, error.message)
  }
}

Promise responses use normal optional values for pagination, rate limits, and redirect locations.

Effect API

Install Effect directly when using the Effect entrypoint:

pnpm add @get-air/simkl effect

Create a client Layer

import { Effect, Redacted } from "effect"
import { SimklClient } from "@get-air/simkl/effect"

const SimklLive = SimklClient.Default({
  clientId: "your-client-id",
  appName: "my-app",
  appVersion: "1.0.0",
  accessToken: Redacted.make("the-user-access-token"),
})

const program = Effect.gen(function* () {
  const simkl = yield* SimklClient
  const movie = yield* simkl.movies.details({
    params: { path: { id: "5392" } },
  })

  if (movie._tag === "SimklResponse") {
    yield* Effect.log("Loaded a movie", { title: movie.data.title })
  }
})

Effect.runPromise(program.pipe(Effect.provide(SimklLive)))

client_id, app-name, app-version, and User-Agent are injected automatically. Pass an accessToken to add the bearer header to API requests. Values supplied to Redacted are not included in logs or error rendering.

Direct Effect client

When a Layer is unnecessary, construct a class instance inside an Effect. Every network operation still returns an Effect:

import { Effect } from "effect"
import { Simkl } from "@get-air/simkl/effect"

const program = Effect.gen(function* () {
  const simkl = yield* Simkl.make({
    clientId: "your-client-id",
    appName: "my-app",
    appVersion: "1.0.0",
    fetch: customFetch,
  })

  return yield* simkl.tv.details({ params: { path: { id: "5483" } } })
})

fetch accepts any (Request) => Promise<Response> implementation. Browser fetch, Tauri HTTP, test doubles, proxy transports, and cached transports all use the same seam.

Air applications can pass their shared @get-air/http transport and @get-air/cache store directly:

const simkl = await Simkl.make({
  clientId,
  appName,
  appVersion,
  transport,
  cache,
})

The shared cache adapter preserves Simkl's safe default policy: only public GET resources are cached, and configured addon/user credentials are never included in cache keys.

Tauri v2

Install the optional plugins in the consuming Tauri application:

pnpm tauri add http
pnpm tauri add store
pnpm add @tauri-apps/plugin-http @tauri-apps/plugin-store

Then create a Rust-backed client with optional persistent caching:

import { Effect, Option } from "effect"
import { makeTauriSimkl } from "@get-air/simkl/effect/tauri"

const program = Effect.gen(function* () {
  const simkl = yield* makeTauriSimkl(
    {
      clientId: "your-client-id",
      appName: "my-tauri-app",
      appVersion: "1.0.0",
    },
    {
      http: { connectTimeout: 10_000, maxRedirections: 5 },
      cache: {
        storePath: "simkl-http-cache.json",
        defaultTtlMs: 15 * 60 * 1_000,
      },
    },
  )

  const movie = yield* simkl.movies.details({ params: { path: { id: "5392" } } })

  // Cache management is available when caching was enabled.
  yield* Option.match(simkl.cache, {
    onNone: () => Effect.void,
    onSome: (cache) => cache.save(),
  })

  return movie
})

Use cache: true for all defaults, omit cache to disable it, or provide options. The built-in policy caches only public GET routes and CDN data; it does not cache Sync, Scrobble, Ratings, settings, OAuth/PIN, POST, or DELETE traffic. Cache keys are SHA-256 hashes and never contain bearer tokens. Cache failures are fail-open, so storage trouble does not take the API offline.

Tauri capabilities must permit the three Simkl origins:

{
  "permissions": [
    {
      "identifier": "http:default",
      "allow": [
        { "url": "https://api.simkl.com/**" },
        { "url": "https://simkl.com/**" },
        { "url": "https://data.simkl.in/**" }
      ]
    },
    "store:default"
  ]
}

Simkl requires User-Agent, which is a forbidden Fetch header. Enable Tauri HTTP's unsafe-headers Rust feature so the plugin does not discard it:

[dependencies]
tauri-plugin-http = { version = "2", features = ["unsafe-headers"] }

If persistent caching is not wanted, pass the plugin transport directly:

import { tauriFetch } from "@get-air/simkl/effect/tauri"

const simkl = yield* Simkl.make({
  clientId,
  appName,
  appVersion,
  fetch: tauriFetch,
})

For environment-based applications, provide SimklClient.layerConfig and set:

SIMKL_CLIENT_ID=your-client-id
SIMKL_APP_NAME=my-app
SIMKL_APP_VERSION=1.0.0
SIMKL_ACCESS_TOKEN=optional-user-token

Endpoint groups

After yielding SimklClient, the following typed groups are available:

| Group | Methods | | --- | --- | | anime | details, airing, best, episodes, genres, premieres | | auth | authorize, requestPin, exchangeToken, pollPin | | calendar | rolling, monthly | | changes | recent | | movies | details, genres | | ratings | community | | redirect | resolve | | scrobble | checkIn, pause, start, stop | | search | byFile, byId, random, byText | | sync | activities, addToList, allItems, addHistory, removeHistory, deletePlayback, allPlayback, playbackByType, addRatings, removeRatings, userRatings, watched | | trending | combined, byType, dvd | | tv | details, airing, best, episodes, genres, premieres | | users | recentlyWatchedBackground, settings, stats |

The lower-level get, post, and del methods accept every generated OpenAPI path and provide the same inference. They are useful when regenerated types pick up a new Simkl endpoint before a named convenience method is released.

const result = yield* simkl.get("/search/{type}", {
  params: {
    path: { type: "movie" },
    query: { q: "The Matrix", extended: "full" },
  },
})

Authentication

Confidential OAuth

Use auth.authorize for the browser-facing authorization URL and auth.exchangeToken for the code exchange. Keep client_secret on a trusted server. Simkl tokens are long-lived and do not use a refresh-token rotation flow.

Public PKCE

Public apps must not ship a client secret. makePkcePair generates a verifier and S256 challenge with Web Crypto:

const pkce = yield* makePkcePair()

yield* simkl.auth.authorize({
  params: {
    query: {
      redirect_uri: "my-app://oauth",
      response_type: "code",
      code_challenge: pkce.challenge,
      code_challenge_method: pkce.method,
    },
  },
  redirect: "manual",
  parseAs: "text",
})

Persist the verifier until the callback, then pass it as code_verifier to auth.exchangeToken.

PIN/device flow

Call auth.requestPin, display the returned user_code and verification URL, then poll auth.pollPin at the response's interval. Stop immediately when the access token is returned. Simkl may create a new code if a completed or unknown code is polled again.

Redirects

For endpoints that return a 301 or 302, pass redirect: "manual" and parseAs: "text". The Effect succeeds with SimklRedirectResponse; its location is an Option<string>.

const redirect = yield* simkl.redirect.resolve({
  params: { query: { to: "simkl", type: "movie", tmdb: 603 } },
  redirect: "manual",
  parseAs: "text",
})

Errors and rate limits

Failures remain in the Effect error channel and can be handled precisely:

yield* simkl.sync.activities().pipe(
  Effect.catchTag("SimklUnauthorizedError", () => beginLogin),
  Effect.catchTag("SimklRateLimitExceededError", (error) =>
    Option.match(error.retryAfterSeconds, {
      onNone: () => Effect.sleep("1 second"),
      onSome: (seconds) => Effect.sleep(`${seconds} seconds`),
    }),
  ),
)

Successful responses expose parsed pagination and rateLimit metadata as Effect Options. Simkl currently limits applications and user tokens to 10 GET requests/second and 1 POST request/second; batch sync writes and use sync.activities before downloading user lists.

Images

Image helpers follow Simkl's recommended wsrv.nl proxy, size suffixes, quality 90, and documented fallbacks:

import { fanartUrl, posterUrl } from "@get-air/simkl"

const poster = posterUrl(movie.poster, "c")
const hero = fanartUrl(movie.fanart, "medium")

Regenerate from OpenAPI

pnpm generate

Generation downloads Simkl's current OpenAPI document, repairs its known ErrorEnvelope/ErrorResponse alias, and emits immutable types plus runtime operation metadata. The contract test fails if the generated operation set and the convenience catalog diverge.

Tests

pnpm test
pnpm check
pnpm build

The opt-in live test reads credentials through Effect Config:

SIMKL_CLIENT_ID=... \
SIMKL_APP_NAME=get-air-simkl-tests \
SIMKL_APP_VERSION=0.1.0 \
pnpm test:integration

No credential is stored in the repository.