cloud_queue
Better MediaDeveloper docs

API

The public surface of the framework: createBetterMedia, the runtime object, and how to call it from your server.

When you call createBetterMedia, you get a runtime object. That object is the entire integration surface — every upload, file operation, and background job goes through it. Your application code talks to the runtime, not to adapters directly.

createBetterMedia

Import the factory from @better-media/framework and pass a single config object. It wires storage, database, plugins, and jobs together and returns the runtime.

lib/media.ts
import { createBetterMedia } from "@better-media/framework";
import { FileSystemStorageAdapter } from "@better-media/adapter-storage-filesystem";
import { memoryDatabase } from "@better-media/adapter-db-memory";
import { validationPlugin } from "@better-media/plugin-validation";

export const media = createBetterMedia({
  storage: new FileSystemStorageAdapter({ baseDir: "./uploads" }),
  database: memoryDatabase(),
  plugins: [validationPlugin({ maxSize: "10mb" })],
});
OptionRequiredDescription
storageYesStorage adapter — where bytes land
databaseYesDatabase adapter — where media records are written
pluginsNoArray of plugins that extend the pipeline
jobsNoJob adapter for background work; defaults to in-process if omitted
fileHandlingNoe.g. { maxBufferBytes: 50_000_000 } — limits before streaming kicks in

Runtime namespaces

createBetterMedia returns a BetterMediaRuntime grouped by concern:

NamespaceRole
media.uploadIngest bytes or references, presigned upload flow, completion after direct-to-storage upload
media.filesRead, mutate, and reprocess files by media record id
media.systemHealth check and destructive admin helpers
media.runBackgroundJobExecute a queued work item from your worker — see Background Jobs

media.upload

Ingest inputs

upload.ingest accepts a file source and metadata:

await media.upload.ingest({
  file: { path: req.file.path },           // or: { buffer }, { stream }, { url }
  metadata: {
    filename: req.file.originalname,
    mimeType: req.file.mimetype,
    size: req.file.size,
    context: { userId: req.user.id },      // arbitrary — passed into plugins
  },
  key: "avatars/photo.jpg",               // optional; derived from filename if omitted
});

File source shapes:

ShapeDescription
{ buffer: Buffer }Raw bytes already in memory
{ stream: Readable }Node.js readable stream
{ path: string }Temp file path; deleted after ingest by default (deleteAfterUpload: true)
{ url: string; mode?: "import" | "reference" }import pulls bytes into storage; reference records the URL without storing bytes

Convenience methods (fromBuffer, fromStream, fromPath, fromUrl) wrap ingest — same options, file source is the first argument.

Successful ingest returns a MediaResult: id, key, status, and metadata.

Example

routes/upload.ts
import type { Request, Response } from "express";
import { media } from "../lib/media";

export async function postUpload(req: Request, res: Response) {
  if (!req.file) {
    res.status(400).json({ error: "No file" });
    return;
  }

  const result = await media.upload.ingest({
    file: { path: req.file.path },
    metadata: {
      filename: req.file.originalname,
      mimeType: req.file.mimetype,
      size: req.file.size,
    },
  });

  res.json({ id: result.id, key: result.key, status: result.status });
}

Presigned uploads

For direct browser-to-storage uploads, your server issues a presigned token and steps aside:

// 1. Issue token
const presign = await media.upload.requestPresignedUpload("user/123/avatar.png", {
  contentType: "image/png",
});

// 2. Browser uploads to presign.url (adapter-specific)

// 3. Run pipeline after bytes land in storage
const result = await media.upload.complete("user/123/avatar.png", {
  filename: "avatar.png",
  mimeType: "image/png",
  context: { userId: "123" },
});

The browser handles step 2 directly against the presigned URL — a standard PUT or POST depending on the storage adapter.

media.files

All methods take a media record id (the id from MediaResult) as their first argument. The runtime resolves the storage key internally.

MethodReturnsNotes
get(id)MediaRecordThrows if not found
exists(id)boolean
getSize(id)number (bytes)
getUrl(id, options?)stringRequires adapter URL support; options accepts expiresIn (seconds)
download(id)Buffer
stream(id)ReadableRequires adapter streaming support
delete(id)voidRemoves from storage and database
deleteMany(ids)voidBatched delete
move(id, newKey)voidRequires adapter support
copy(id, newKey)MediaResultRequires adapter support
reprocess(id, metadata?)MediaResultRe-runs the plugin pipeline for an existing file

If an adapter does not support an optional capability, the runtime throws a clear error rather than returning a partial result.

media.system

MethodDescription
checkConnection()Validates storage reachability; resolves optimistically when the adapter has no health check
clearStorage()Destructive — wipes storage and removes all media rows. Development and controlled admin use only

Error types

ClassPackageWhen it's thrown
ValidationError@better-media/frameworkFile fails a plugin validation rule (size, type, dimensions, etc.)
PipelineError@better-media/frameworkUnhandled failure inside the plugin pipeline
AdapterError@better-media/frameworkStorage or database adapter operation fails
RateLimitError@better-media/plugin-rate-limitUpload rate limit exceeded — see Rate Limiting

Map known errors to status codes in your HTTP layer; rethrow or log the rest:

import { ValidationError, AdapterError } from "@better-media/framework";

try {
  const result = await media.upload.ingest({ ... });
} catch (e) {
  if (e instanceof ValidationError) return res.status(400).json({ error: e.message });
  if (e instanceof AdapterError) return res.status(502).json({ error: "Storage error" });
  throw e;
}

Type exports

All types are importable from @better-media/framework:

TypeDescription
BetterMediaRuntimeThe full runtime object returned by createBetterMedia
IngestInputInput shape for upload.ingest
MediaResultReturn value of ingest, complete, copy, and reprocess
MediaRecordA media row as stored in the database
MediaMetadataThe metadata object on IngestInput and MediaResult
BackgroundJobPayloadPayload shape passed to runBackgroundJob
PluginContextContext object passed into every plugin hook
PresignedUploadResultReturn value of requestPresignedUpload

On this page