Node.js SDK
The @filenest/node SDK provides a typed client for all FileNest REST API operations. Use it in any Node.js runtime — Next.js server components, Express, Fastify, serverless functions, scripts.
Installation
pnpm add @filenest/nodeSetup
import { FileNest } from "@filenest/node";
const fn = new FileNest({
apiKey: process.env.FILENEST_API_KEY!,
projectId: process.env.FILENEST_PROJECT_ID!,
// baseUrl: "http://localhost:8000" // defaults to https://filenest.drgodly.com
});Files
Upload
import { createReadStream } from "fs";
// From a Buffer or stream
const file = await fn.files.upload({
filename: "report.pdf",
data: createReadStream("./report.pdf"),
mimeType: "application/pdf",
folderId: "fld_01j...", // optional
tags: ["finance", "2026"], // optional
metadata: { year: "2026" }, // optional
});
console.log(file.id); // file_01j...
console.log(file.status); // "ready"Files under 5 MB are uploaded in a single request. Files over 5 MB are automatically split into multipart upload parts — no configuration needed.
Download
// Get a presigned URL (default 1 hour TTL)
const { url } = await fn.files.getDownloadUrl(file.id, { ttl: 3600 });
// Stream bytes directly
const stream = await fn.files.download(file.id);
stream.pipe(fs.createWriteStream("./output.pdf"));
// Buffer entire file into memory
const buffer = await fn.files.downloadToBuffer(file.id);List
const { items, total } = await fn.files.list({
status: "ready",
folderId: "fld_01j...",
mimeType: "application/pdf",
tags: ["finance"],
metadata: { year: "2026" }, // server-side metadata filter
sortBy: "created_at",
sortOrder: "desc",
limit: 50,
offset: 0,
});Get, update, delete, restore
const file = await fn.files.get(fileId);
await fn.files.update(fileId, {
filename: "report-2026-final.pdf",
tags: ["archived"],
metadata: { reviewed: true },
});
await fn.files.delete(fileId); // soft delete — file is recoverable
// Restore a soft-deleted file
const restored = await fn.files.restore(fileId);Versions
const { items } = await fn.files.versions.list(fileId);
await fn.files.versions.restore(fileId, versionId);Folders
Create and ensure paths
// Create a single folder (parentFolderId=null → project root)
const folder = await fn.folders.create({
name: "invoices",
parentFolderId: "fld_01j...", // optional — omit for root-level folder
metadata: { department: "finance" },
});
// Idempotent: create every missing path segment, return the leaf folder
const folder = await fn.folders.ensurePath("users/alice/uploads");Resolve paths
// Resolve a path to a folder record (returns null if not found)
const folder = await fn.folders.getByPath("users/alice/uploads");
// Get by ID
const folder = await fn.folders.get(folderId);List and browse
// List all folders in the project
const { items } = await fn.folders.list();
// Filter by name
const { items } = await fn.folders.list({ name: "uploads" });
// List files inside a specific folder
const { items: files } = await fn.folders.listFiles(folderId, {
q: "invoice", // full-text search within the folder
tags: ["finance"],
category: "document",
status: "ready",
limit: 20,
offset: 0,
cursor: lastCursor, // cursor-based pagination (use items' last id)
});Delete
// Fails with 409 if the folder contains files or subfolders
await fn.folders.delete(folderId);Upload Tokens
Issue short-lived tokens for browser-side uploads. The token is returned to the client through your own token endpoint — the API key never reaches the browser.
const token = await fn.uploadTokens.create({
folderId: folder.id, // lock uploads to a specific folder
ownerUserId: session.userId, // stamped on every uploaded file
ownerOrgId: session.orgId,
allowedMimeTypes: ["image/jpeg", "image/png", "image/webp"],
maxSize: 5 * 1024 * 1024, // 5 MB per file
maxFiles: 1,
expiresIn: 600, // 10 minutes
metadata: { uploadedFrom: "profile-settings" }, // merged onto every uploaded file
tags: ["avatar", "user-content"], // merged onto every uploaded file
});
// token.token → "fn_upload_token_..." — return this to the browser
// token.expiresAt → ISO timestamp — return this too so the browser can schedule refreshToken constraints are validated against the project's config at creation time — if the project only allows image/*, passing application/pdf in allowedMimeTypes will return a 422 immediately. At upload time both the token constraints and the project config are enforced independently. See Upload Tokens API reference for the full constraint reconciliation rules.
Webhooks
// Create an endpoint
const webhook = await fn.webhooks.create({
name: "prod-receiver",
url: "https://app.example.com/webhooks/filenest",
events: ["file.uploaded", "file.ready", "file.failed"],
});
// webhook.signingSecret — store safely, shown only once
// List, get, update, delete
const { items } = await fn.webhooks.list();
const wh = await fn.webhooks.get(webhookId);
await fn.webhooks.update(webhookId, { status: "disabled" });
await fn.webhooks.delete(webhookId);
// Delivery history (limit/offset pagination)
const { items: deliveries } = await fn.webhooks.listDeliveries(webhookId, { limit: 20 });
// Verify an incoming signature
const isValid = fn.webhooks.verify(rawBody, req.headers["x-filenest-signature"], secret);Resumable Uploads
fn.uploads gives you manual control over the multipart session lifecycle. Use this when you need to resume an interrupted upload — for example, a large file upload that failed mid-way.
For most cases, fn.files.upload() handles multipart automatically and you don't need this namespace.
// 1. Create a session and store the uploadId somewhere durable
const session = await fn.uploads.create({
filename: "large-video.mp4",
sizeBytes: fileBuffer.length,
mimeType: "video/mp4",
folderId: "fld_01j...",
tags: ["video", "raw"],
metadata: { uploadedBy: "user_abc" },
});
// session.uploadId → persist this to resume later
// 2. Upload all parts (or resume from where you left off)
const file = await fn.uploads.resume(session.uploadId, {
data: fileBuffer,
onProgress: ({ percentage }) => console.log(`${percentage}%`),
});
// 3. If you need to cancel
await fn.uploads.abort(session.uploadId);Search
const { hits, total, facets } = await fn.search.query({
q: "invoice",
filters: { status: "ready", tags: ["finance"] },
facets: ["mimeType", "tags"],
sortBy: "created_at",
sortOrder: "desc",
limit: 20,
offset: 0,
});
// Paginate with an async generator
for await (const file of fn.search.iterate({ q: "invoice" })) {
console.log(file.filename);
}