Node.js and TypeScript media SDK

dreep is the official Node.js and TypeScript SDK for Dreep. Install it with npm install dreep, then upload files from your server, presign direct uploads for browsers, build transform URLs and signed links without a network call, and run OCR or background removal. It has zero dependencies, ships ESM and CommonJS builds with its own types, and needs Node.js 18 or later.

Install and run your first upload

Create an API key on the API Keys page of the dashboard and keep it in an environment variable on your server. The signing secret is optional and only needed for signedUrl(). The cURL tab shows the request the SDK makes for you.

// npm install dreep
import fs from "node:fs";
import { Dreep } from "dreep";

const dreep = new Dreep({
  apiKey: process.env.DREEP_API_KEY, // "drp_live_..."
  signingSecret: process.env.DREEP_SIGNING_SECRET, // optional
});

// Resized and converted to WebP before it is stored
const asset = await dreep.upload({
  file: fs.createReadStream("hero.jpg"),
  folder: "marketing/2026",
  transform: { width: 1200, format: "webp" },
});

asset.url; // https://cdn.dreep.cloud/api/v1/fetch/2f928a3f-...
dreep.url(asset, { width: 400 }); // no network call

What the package gives you

A thin client over the REST API that removes the plumbing: multipart bodies, the response envelope, BIGINT strings, signature maths and error mapping.

Zero dependencies

Built on native fetch, FormData and node:crypto. Nothing else lands in your lockfile, so the SDK adds no supply-chain surface to your server.

Typed transforms

Written in TypeScript and shipped with .d.ts files. fit, format, gravity, rotate and videoCodec are typed unions, so fit: "squish" or rotate: 45 is a compile error rather than an ignored query parameter.

ESM and CommonJS

import and require both resolve. Releases publish from CI with npm provenance, linking each version to the commit that built it.

Server-only by design

The API key can read, upload and delete everything in a project, so the constructor throws if it detects a browser. Client uploads go through presigned URLs your server issues.

Server uploads

upload() accepts a Buffer, a Readable stream, a Blob or a File. Choose a destination with folder (a slug path, created if missing), key (an S3-style key whose last segment is the filename) or folderId. Omit all three and the asset lands in the project's default folder.

// A Buffer carries no name, so pass one
const avatar = await dreep.upload({
  file: buffer,
  filename: "me.jpg",
  key: "avatars/2026/q1/me.webp",
  transform: { width: 512, height: 512, fit: "cover", format: "webp", quality: 80 },
});

// Fail instead of creating folders for an unknown path
await dreep.upload({ file, folder: "invoices/2026", autoCreateFolders: false });

// Apply a saved preset on the way in
await dreep.upload({ file, presetKey: "thumbnail" });

A transform passed to upload() runs before storage: the result is what gets stored, the original is not kept, and it counts against your monthly transformation quota. To keep the original and derive sizes later, upload without a transform and use url().

A Node Readable is read into memory before sending, because fetch can't stream a multipart body. For video and other large files, prefer the presigned flow below, which sends the bytes straight to Dreep storage. Each file can be up to 500 MB.

Browser uploads through your backend

Browsers never hold your API key. Your server checks who is uploading, asks Dreep for a single-use upload URL, and the browser PUTs the file there directly. The URL is valid for 10 minutes, and the Content-Type and Content-Length of the PUT must match what you presigned.

  1. Browser asks

    name, type and size

  2. Server presigns

    presignUpload()

  3. Browser PUTs

    bytes to uploadUrl

  4. Server confirms

    confirmUpload()

  5. Asset ready

    CDN URL returned

Until confirmUpload() runs, the asset stays pending and is excluded from media listings.

Using the App Router? The Next.js integration guide shows the same two calls as route handlers, plus serving transformed images from Dreep URLs.

Presign on the server, PUT from the browser

The server half wraps two SDK calls behind your own authenticated routes. The browser half is plain fetch with no Dreep code at all.

// Server: behind your own auth, after your own quota checks
const startUpload = async ({ userId, filename, contentType, sizeBytes }) => {
  const upload = await dreep.presignUpload({
    filename,
    contentType,
    sizeBytes,
    folder: `users/${userId}`,
  });

  return { id: upload.id, uploadUrl: upload.uploadUrl };
};

const finishUpload = ({ id }) => dreep.confirmUpload({ id });

// Browser: no API key, only the single-use URL
const res = await fetch("/api/uploads", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ filename: file.name, contentType: file.type, sizeBytes: file.size }),
});
const { id, uploadUrl } = await res.json();

await fetch(uploadUrl, { method: "PUT", body: file, headers: { "Content-Type": file.type } });
await fetch(`/api/uploads/${id}/confirm`, { method: "POST" });

Pass the file's SHA-256 as contentHash when presigning and Dreep tells you if it already stores those bytes: alreadyExists comes back true with no uploadUrl, so you skip the PUT and confirm straight away.

Transform URLs, presets and signed links

url() appends transform parameters to the URL the API returned for an asset. It is pure string work with no API key and no request, so it is safe in a render path. Each variant is generated on first request and served from cache afterwards.

dreep.url(asset, { width: 800, format: "avif", quality: 70 });
dreep.url(asset, { width: 1200, height: 630, fit: "cover", gravity: "attention" });
dreep.url(storedUrlString, { width: 400 }); // works on a saved URL too

// Save a recipe once, then reference it by key
await dreep.createPreset({
  key: "thumbnail",
  name: "Thumbnail",
  transform: { width: 200, height: 200, fit: "cover", format: "webp" },
});
dreep.url(asset, { preset: "thumbnail" }); // ...?p=thumbnail

// Assets in a signed folder: time-limited, signed locally
dreep.signedUrl(asset, { expiresIn: 3600 });
dreep.signedUrl(asset, { expiresIn: 900, transform: { width: 800 } });

signedUrl() needs signingSecret in the client config and takes the asset rather than a bare URL, because the signature is computed over its id. expiresIn is in seconds. Anyone holding the link can fetch the asset until it expires, so treat it as a bearer token and keep expiries short.

Changing a preset changes what every call site using it delivers, without a deploy. Plans include 3, 10 or 50 presets on Free, Starter and Pro.

OCR and background removal

extractText() runs OCR over an uploaded image or PDF and returns the text plus line and word blocks with bounding boxes. With a folder, the text is also stored as a .txt asset. removeBackground() stores a transparent cutout as a new asset.

const { text, blocks, savedAsset } = await dreep.extractText({
  file: fs.createReadStream("receipt.jpg"),
  folder: "receipts/2026", // optional: also save the text
});

// OCR on an asset already in Dreep: request the txt format
const ocr = await fetch(dreep.url(asset, { format: "txt" }));
const pageText = await ocr.text();

const cutout = await dreep.removeBackground({
  file: fs.createReadStream("shoe.jpg"),
  folder: "products",
});
dreep.url(cutout, { bg: "ffffff" }); // any background colour, no new removal

OCR is billed per page. Every plan includes 25 background removals, and both have 1,000-unit add-on packs for $4.99.

Errors, retries and cancellation

Every failure throws a subclass of DreepError carrying the HTTP status and the API's message. Requests time out after 30 seconds; uploads have no default timeout because a large file can take minutes, so bound them with an AbortSignal. Every method accepts signal.

ClassStatusRaised when
DreepAuthError401Missing, malformed or revoked API key
DreepValidationError400Invalid parameters; validationErrors lists each field
DreepNotFoundError404Unknown asset, folder, preset or upload
DreepConflictError409Confirming an upload whose bytes haven't landed yet
DreepLimitError402Plan limit reached; carries featureKey, used and limit
DreepConnectionErrornoneNetwork failure, timeout or an aborted signal
Error classes thrown by the dreep package

Handling a plan limit

Safe, idempotent reads are retried twice with exponential backoff. Uploads and other writes are never retried automatically, because a repeat could store a duplicate and bill a second transformation.

import { DreepError, DreepLimitError } from "dreep";

try {
  const asset = await dreep.upload({
    file,
    folder: "avatars",
    signal: AbortSignal.timeout(120_000),
  });
  return asset.url;
} catch (error) {
  if (error instanceof DreepLimitError) {
    // e.g. storage full: upgrade or buy an add-on, there is no automatic overage
    console.log(error.featureKey, error.used, error.limit);
  } else if (error instanceof DreepError) {
    console.log(error.status, error.message);
  }
}

Who this is for

  • Node.js or TypeScript backends, including Next.js route handlers and server actions.
  • Apps where users upload from the browser through presigned URLs your server issues.
  • Teams that want typed transform parameters and no third-party packages at runtime.
  • Workers and scripts that bulk upload, list or clean up media with listAllMedia().

When Dreep is not the right fit

  • You want to call Dreep from browser or React Native code with the API key. The SDK refuses to run in a browser.
  • You run Node.js older than 18. The SDK relies on native fetch.
  • You need a UI upload widget. The package is an API client; build the picker yourself.

Frequently asked questions

Does the dreep package work with both import and require?

Yes. It ships an ESM build and a CommonJS build, with type declarations for both, so import { Dreep } from "dreep" and require("dreep") resolve on Node.js 18 or later.

How do I upload from the browser without exposing my API key?

Call presignUpload() on your server with the file's name, type and size, return the id and uploadUrl to the browser, let it PUT the file to uploadUrl, then call confirmUpload({ id }) on your server. The upload URL is single-use and valid for 10 minutes.

Does url() make a request to Dreep?

No. It appends query parameters such as width, format and quality to the asset's CDN URL. The transformed variant is generated the first time that URL is requested and served from cache after that.

What is the difference between transform on upload and url()?

A transform passed to upload() runs once before storage and replaces the original. url() leaves the original intact and derives any number of sizes and formats from it on request. Both count against the monthly transformation quota when a new variant is generated.

Which errors should I handle?

Catch DreepError for everything. Branch on DreepLimitError (402) to tell users a plan limit was reached, DreepConflictError (409) to retry a confirm, and DreepConnectionError for network failures, timeouts and aborted signals.

Is the SDK open source?

Yes. It is MIT licensed and developed on GitHub as IndigoSoftwares21/dreep-js, and each npm release is published from CI with provenance.

Related

Install dreep and upload your first file

Create a free project for 1 GB of storage and 100 image transformations a month, copy an API key, and run npm install dreep.