Open-source project
unsplash/unsplash-js avatar
unsplash/unsplash-js

unsplash-js: a thin, typed wrapper around the Unsplash API

🤖 Official JavaScript wrapper for the Unsplash API

2,195 stars161 forksTypeScriptMIT

At a glance

What is it?
An official TypeScript client that generates its own types from the Unsplash OpenAPI document, delegates transport to openapi-fetch, and keeps its published bundle under five kilobytes.
Who is it for?
unsplash-js is worth using when you want the Unsplash API's response shapes as real TypeScript types without hand-maintaining an SDK, and not much else. It is a single dependency over openapi-fetch, the surface is five verb methods, and the API guidelines obligations around hotlinking and attribution sit alongside the code rather than inside it.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 6 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A thin layer over openapi-fetch with types generated from a spec

The clearest way to understand this library is to notice what it is not. It does not implement HTTP calls, it does not hand-write request and response types, and it does not model Unsplash's resources as classes. It is a preconfigured `openapi-fetch` instance, and the README says so directly.

That choice shows up in `package.json`. There is exactly one runtime dependency, `openapi-fetch` at `^0.17.0`, and a dev dependency set that includes `openapi-typescript`, `tsdown`, `oxlint`, `oxfmt`, `vitest`, `typescript` and `size-limit`. The `dist` folder is the only thing published, and the exports map ships both ESM and CommonJS entry points with matching declaration files for each.

The type generation is the part worth understanding. The `gen` script runs `openapi-typescript` against `./public.json`, which is the OpenAPI document checked into the repository root, and a second step pipes that same document through a jq program into an alias declaration file. So the types are a build artifact of a machine-readable spec, not something maintained by hand. When Unsplash changes the API, the fix is a spec update rather than a diff across hundreds of lines of handwritten interfaces.

Two release channels are worth noting separately. `package.json` reports version 8.0.1, and the repository uses pnpm with a `pnpm-lock.yaml` and a `pnpm-workspace.yaml`. The GitHub releases, however, stop at v7.0.18 from May 2023. If you are pinning a version, pin the npm version; the tag history will mislead you.

Creating an instance and choosing where requests run

Installation supports the three package managers without further comment:

bash
$ npm i --save unsplash-js

Creating a client is passing an access key:

ts
import { createApi } from "unsplash-js";

const serverApi = createApi({
  accessKey: "MY_ACCESS_KEY",
});

In the browser, the constructor takes a `baseUrl` pointing at your own server instead:

ts
const browserApi = createApi({
  baseUrl: "https://mywebsite.com/unsplash-proxy",
});

The README states the constraint plainly and in bold: if you use this in the browser you must proxy requests through your server by setting `baseUrl`, which is how authentication with your access key happens and how the key stays out of client code. And the two parameters are mutually exclusive: set `baseUrl` and `accessKey` is unnecessary, and the reverse also holds.

This is the single most consequential thing in the README, and it is worth reading as a requirement rather than a suggestion. A client-side access key is a leaked key, and the library is structured so that getting this wrong takes editing the constructor rather than adding a proxy later.

The methods that come back are named after HTTP verbs, so there are `.GET`, `.POST`, `.PUT`, `.DELETE` and `.PATCH`, and each takes a `params` object alongside standard fetch options. Paths use the same templating style as the OpenAPI document:

ts
unsplash.GET("/photos/{assetSlug}", {
  params: { path: { assetSlug: "123" } },
});

Two outcomes per request, and the raw response is available

Every call resolves to a result object with one of two shapes. On success `result.data` is populated. On a server error `result.error` is present and carries error details, and the README's example reads `result.error.errors[0]`. A third field, `result.response`, holds the raw `Response` when you need headers rather than a parsed body.

That third field is not decoration. The README's feed example uses it to read a total count out of a header:

ts
const result = await unsplash.GET("/users/{username}/photos", {
  params: { path: { username: "foo" } },
});

const feed = result.data;
const total = parseInt(result.response.headers.get("x-total"));

Pagination on the list endpoints is a header, not a field in the body, so without `result.response` you cannot build a pager. That is a small design consequence that will matter the first time you build an infinite scroll.

Fetch options layer the way you would hope. Anything passed to `createApi` is sent with every request, and per-request options override them, so a global header set once and a one-off header on a single call both work without special casing. An `AbortSignal` from an `AbortController` is passed straight through, and the README's example shows the `AbortError` case handled by name.

One caveat the README does raise: ordinary fetch exceptions still throw. A rejected network request is not folded into `result.error`, so code that only checks `result.error` is not handling every failure path.

Swapping fetch for tests, older runtimes or polyfills

Because the client delegates to global fetch, you can replace that global or pass an implementation to the constructor. Globally looks like this:

ts
import fetch from "node-fetch";
global.fetch = fetch;

Or per instance:

ts
const unsplash = createApi({
  accessKey: "MY_ACCESS_KEY",
  fetch: nodeFetch,
});

The README names the three reasons this matters: mocking fetch in tests, targeting an older runtime, and providing a custom implementation. The `engines` field in `package.json` requires Node 20 or newer, which sets the floor for anyone on the server, and the fact that fetch replacement is documented at all suggests the library expects some environments to need it.

For tests, the README defers to the openapi-fetch documentation rather than describing a mocking recipe, which is the right call if you have already read openapi-fetch's testing page and the wrong one if you have not.

Authentication has two layers worth separating. Passing `accessKey` sets the `Authorization` header for public authentication automatically. If you need user authentication or dynamic client registration, you set the `Authorization` header yourself on each request, following the requirements of whichever scheme you pick. That means a user-authenticated flow does not go through the `accessKey` shortcut at all.

A five kilobyte budget, enforced by CI

The repository carries a `size-limit` configuration with a five kilobyte ceiling on both the ESM and CommonJS builds, and a `size` npm script to check it. A limit that fails the build is worth more than a badge, and the presence of `@size-limit/preset-small-lib` as a dev dependency means this is wired into the workflow rather than aspirational.

The tooling choices are recent and unusually consistent. `tsdown` handles the build with a watch mode for development, `oxlint` and `oxfmt` handle linting and formatting in place of the eslint and prettier combination, and `vitest` runs the tests. Type checking is a separate `tsc` script. There are Nix files, `flake.nix` and `flake.lock`, at the root, so the toolchain itself is pinned for contributors who want it.

The declared types target TypeScript 4.5 and above, while the dev dependency is TypeScript 5.8. That gap is normal and deliberate: the floor is what consumers need, the dev version is what the project builds with.

The README spends its final section on editor completion, with four screenshots showing path and params completion and response completion in VS Code. That is a deliberate argument: the library's value is that because it is written in TypeScript with generated types, you get accurate completions whether or not your own project uses TypeScript. Whether that lands depends entirely on how good your editor's inference is.

The API guidelines are a real part of using this client

The README's key links section puts the Unsplash API guidelines before the demos, and lists three requirements with an emphasis on must: hotlink images, attribute photographers, and trigger a download when appropriate. Attribution and hotlinking are behaviours a client library cannot enforce on your behalf, which is why they appear before any code.

That ordering is the honest framing for this repository. It is the official wrapper, but the obligations in the API guidelines sit with the integrator, and the library's job is to give you correctly typed access to the endpoints. Nothing in the response type will stop you from downloading an image and serving it from your own asset host.

On the repository's own state: it is not archived, the last push was 2026-09-21, and it has 27 open issues against 2,194 stars and 162 forks. The published releases, as noted, are stale relative to the package version, and the CHANGELOG is at the root for the details.

For a reader deciding whether to use this or call the REST endpoints directly from your own code, the comparison is narrow. Hand-rolling means no generated types, no consistent error shape and no fetch override hook. Using this means a five kilobyte dependency, a Node 20 floor, and a mandatory server-side proxy in the browser. For anything beyond a one-off script, the trade is usually worth it.

Editorial conclusion

unsplash-js is worth using when you want the Unsplash API's response shapes as real TypeScript types without hand-maintaining an SDK, and not much else. It is a single dependency over openapi-fetch, the surface is five verb methods, and the API guidelines obligations around hotlinking and attribution sit alongside the code rather than inside it. Two things to check before committing: the package requires Node 20 or newer and a fetch implementation you supply if your runtime lacks one, and the GitHub releases stop at v7.0.18 from 2023 while package.json already reads 8.0.1, so track the npm version rather than the release tags. Read the API guidelines page before your first integration, not after.

Frequently asked questions

How do I get an Unsplash API key for unsplash-js?

Register as a developer at unsplash.com/developers and create an application to receive an access key. Then pass it to createApi as accessKey on your server, or proxy requests through your own server and set baseUrl in the browser. The README marks both as things to do before using the API.

Is the Unsplash API free to use with unsplash-js?

The library itself is MIT licensed and published on npm at no cost, and the repository topics include free. Unsplash separately operates a paid Plus tier for premium photography, and the API guidelines on hotlinking, attribution and download triggering apply regardless of which you use.

What Node version does unsplash-js require?

package.json declares engines with node set to >=20. For older runtimes the README documents replacing fetch, either globally with node-fetch or whatwg-fetch, or per instance by passing a fetch implementation to createApi.

Can I use an Unsplash access key directly in the browser?

Not safely, and the README says so explicitly. Browser use requires setting baseUrl to a proxy on your own server, which is both how the access key authenticates requests and how the key stays confidential. If baseUrl is set, accessKey is unnecessary, and the two are mutually exclusive.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. unsplash/unsplash-js on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/unsplash-unsplash-js.svg)](https://hysenlabs.com/projects/unsplash-unsplash-js)