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.
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" })],
});| Option | Required | Description |
|---|---|---|
storage | Yes | Storage adapter — where bytes land |
database | Yes | Database adapter — where media records are written |
plugins | No | Array of plugins that extend the pipeline |
jobs | No | Job adapter for background work; defaults to in-process if omitted |
fileHandling | No | e.g. { maxBufferBytes: 50_000_000 } — limits before streaming kicks in |
Runtime namespaces
createBetterMedia returns a BetterMediaRuntime grouped by concern:
| Namespace | Role |
|---|---|
media.upload | Ingest bytes or references, presigned upload flow, completion after direct-to-storage upload |
media.files | Read, mutate, and reprocess files by media record id |
media.system | Health check and destructive admin helpers |
media.runBackgroundJob | Execute 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:
| Shape | Description |
|---|---|
{ 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
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.
| Method | Returns | Notes |
|---|---|---|
get(id) | MediaRecord | Throws if not found |
exists(id) | boolean | |
getSize(id) | number (bytes) | |
getUrl(id, options?) | string | Requires adapter URL support; options accepts expiresIn (seconds) |
download(id) | Buffer | |
stream(id) | Readable | Requires adapter streaming support |
delete(id) | void | Removes from storage and database |
deleteMany(ids) | void | Batched delete |
move(id, newKey) | void | Requires adapter support |
copy(id, newKey) | MediaResult | Requires adapter support |
reprocess(id, metadata?) | MediaResult | Re-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
| Method | Description |
|---|---|
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
| Class | Package | When it's thrown |
|---|---|---|
ValidationError | @better-media/framework | File fails a plugin validation rule (size, type, dimensions, etc.) |
PipelineError | @better-media/framework | Unhandled failure inside the plugin pipeline |
AdapterError | @better-media/framework | Storage or database adapter operation fails |
RateLimitError | @better-media/plugin-rate-limit | Upload 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:
| Type | Description |
|---|---|
BetterMediaRuntime | The full runtime object returned by createBetterMedia |
IngestInput | Input shape for upload.ingest |
MediaResult | Return value of ingest, complete, copy, and reprocess |
MediaRecord | A media row as stored in the database |
MediaMetadata | The metadata object on IngestInput and MediaResult |
BackgroundJobPayload | Payload shape passed to runBackgroundJob |
PluginContext | Context object passed into every plugin hook |
PresignedUploadResult | Return value of requestPresignedUpload |