@betechspark/fileserver-client
v0.1.1
Published
TypeScript client for the betechspark File Server — HTTP gateway, Zod-validated responses, framework-agnostic use cases.
Maintainers
Readme
@betechspark/fileserver-client
TypeScript client for the betechspark File Server — HTTP gateway, Zod-validated responses, and framework-agnostic use cases.
npm: @betechspark/fileserver-client · Related: fileserver-server · fileserver-react
Installation
Requires a running betechspark File Server (default http://127.0.0.1:3004).
npm install @betechspark/fileserver-client
# or: pnpm add / yarn add @betechspark/fileserver-clientEnvironment variables
| Variable | Where | Purpose |
|----------|-------|---------|
| NEXT_PUBLIC_FILESERVER_URL | Browser / SSR fallback | Public File Server URL |
| FILESERVER_INTERNAL_URL | Node / Docker / K8s | Internal service URL for server-side calls |
On Windows, prefer http://127.0.0.1:3004 over localhost (IPv6 vs IPv4).
Quick Start
import {
createHttpFileserverGateway,
uploadFile,
tusUpload,
getFileMeta,
deleteFile,
} from '@betechspark/fileserver-client';
const gateway = createHttpFileserverGateway({
baseUrl: process.env.NEXT_PUBLIC_FILESERVER_URL ?? 'http://127.0.0.1:3004',
});
// Standard multipart upload
const result = await uploadFile(gateway, {
file,
filename: file.name,
ownerId: 'user:abc',
mimeType: file.type,
visibility: 'private', // 'public' | 'private' | 'unlisted'
});
// → { id, hash, deduped }
// Resumable upload for large files (default chunk: 4 MiB)
const tusResult = await tusUpload(gateway, {
file,
filename: file.name,
ownerId: 'user:abc',
chunkSize: 4 * 1024 * 1024,
});
const meta = await getFileMeta(gateway, result.id);
await deleteFile(gateway, result.id);Examples
Node.js — upload a file from disk
import { readFileSync } from 'node:fs';
import { File } from 'node:buffer';
import {
createHttpFileserverGateway,
uploadFile,
getFileMeta,
} from '@betechspark/fileserver-client';
const gateway = createHttpFileserverGateway({
baseUrl: process.env.FILESERVER_INTERNAL_URL ?? 'http://127.0.0.1:3004',
});
const bytes = readFileSync('./invoice.pdf');
const file = new File([bytes], 'invoice.pdf', { type: 'application/pdf' });
const { id, hash, deduped } = await uploadFile(gateway, {
file,
filename: 'invoice.pdf',
ownerId: 'org:acme',
visibility: 'private',
});
console.log({ id, hash, deduped });
console.log(await getFileMeta(gateway, id));Next.js App Router — server-side upload (Route Handler)
Keep uploads on the server so you can attach auth and never expose internal URLs.
// app/api/files/upload/route.ts
import { createHttpFileserverGateway, uploadFile } from '@betechspark/fileserver-client';
import { NextResponse } from 'next/server';
const gateway = createHttpFileserverGateway({
baseUrl: process.env.FILESERVER_INTERNAL_URL ?? 'http://127.0.0.1:3004',
});
export async function POST(request: Request) {
const form = await request.formData();
const file = form.get('file');
const ownerId = form.get('ownerId');
if (!(file instanceof File) || typeof ownerId !== 'string') {
return NextResponse.json({ error: 'file and ownerId required' }, { status: 400 });
}
const result = await uploadFile(gateway, {
file,
filename: file.name,
ownerId,
mimeType: file.type,
visibility: 'private',
});
return NextResponse.json(result);
}Client call:
const body = new FormData();
body.set('file', file);
body.set('ownerId', session.user.id);
await fetch('/api/files/upload', { method: 'POST', body });Express — minimal upload endpoint
import express from 'express';
import multer from 'multer';
import { createHttpFileserverGateway, uploadFile } from '@betechspark/fileserver-client';
const app = express();
const upload = multer({ storage: multer.memoryStorage() });
const gateway = createHttpFileserverGateway({
baseUrl: process.env.FILESERVER_INTERNAL_URL ?? 'http://127.0.0.1:3004',
});
app.post('/upload', upload.single('file'), async (req, res) => {
if (!req.file) return res.status(400).json({ error: 'file required' });
const file = new File([req.file.buffer], req.file.originalname, {
type: req.file.mimetype,
});
const result = await uploadFile(gateway, {
file,
filename: req.file.originalname,
ownerId: req.body.ownerId ?? 'anonymous',
visibility: 'private',
});
res.json(result);
});Visibility — public vs private downloads
| visibility | Direct URL | Signed URL needed |
|--------------|------------|-------------------|
| public | {baseUrl}/files/{id} works | Optional |
| private / unlisted | Blocked without signature | Yes — use @betechspark/fileserver-server on the server |
// Public asset — no signing
const publicUrl = `${baseUrl}/files/${meta.id}`;
// Private asset — sign on the server, never in the browser
// See fileserver-server README for signDownloadUrl()Handle errors
import { FileserverError, getFileMeta } from '@betechspark/fileserver-client';
try {
await getFileMeta(gateway, fileId);
} catch (err) {
if (err instanceof FileserverError) {
if (err.code === 'NotFound') {
// 404 — file deleted or wrong id
} else if (err.code === 'Forbidden') {
// 403 — no access
}
}
throw err;
}Unit tests — mock the gateway
Implement FileserverGateway for fast tests without a running File Server:
import type { FileserverGateway } from '@betechspark/fileserver-client';
import { uploadFile } from '@betechspark/fileserver-client';
const mockGateway: FileserverGateway = {
upload: async () => ({ id: 'test-id', hash: 'abc', deduped: false }),
tusUpload: async () => ({ id: 'test-id', hash: 'abc', deduped: false }),
getMeta: async (id) => ({
id,
ownerId: 'user:test',
filename: 'test.txt',
mimeType: 'text/plain',
sizeBytes: 5,
contentHash: 'abc',
visibility: 'private',
createdAt: new Date().toISOString(),
}),
delete: async () => {},
signDownload: async () => ({ signature: 'mock-sig' }),
};
const result = await uploadFile(mockGateway, {
file: new Blob(['hello']),
filename: 'hello.txt',
ownerId: 'user:test',
});
expect(result.id).toBe('test-id');Full stack pattern
Browser ──POST /api/upload──► Next.js/Express ──uploadFile()──► File Server
Browser ◄──{ id }───────────
Browser ──GET /api/sign-download?fileId──► Server ──signDownloadUrl()──► { url }
Browser ◄──signed url──────────────────────
Browser ──<img src={signedUrl} />────────► File Server (valid exp + sig)For React hooks and UI, use @betechspark/fileserver-react. For HMAC signing, use @betechspark/fileserver-server.
Overview
| Layer | Purpose |
|-------|---------|
| Ports | FileserverGateway interface for uploads, metadata, delete, and signing |
| Use cases | Thin wrappers (uploadFile, tusUpload, getFileMeta, deleteFile) |
| Infrastructure | createHttpFileserverGateway — default HTTP implementation |
| Domain | Zod schemas (FileRecord, UploadResult) and FileserverError |
Use this package in Node, the browser, Edge runtimes, or tests. For React hooks, see @betechspark/fileserver-react. For server-side HMAC signing, see @betechspark/fileserver-server.
Detailed usage (step-by-step)
See USAGE.md for a numbered walkthrough. Summary:
| Step | Topic |
|------|--------|
| 1 | Prerequisites — start File Server, health check |
| 2 | Install and build |
| 3 | Create the gateway |
| 4 | Upload a file (uploadFile) |
| 5 | Large files — TUS (tusUpload) |
| 6 | Read metadata (getFileMeta) |
| 7 | Delete (deleteFile) |
| 8 | Handle FileserverError |
| 9 | What's next — react, server, examples |
Monorepo development
pnpm turbo run build --filter=@betechspark/fileserver-clientHTTP endpoint mapping
| Gateway method | HTTP | Path / notes |
|----------------|------|----------------|
| upload | POST | /files (multipart: file, owner_id, optional mime_type, visibility) |
| tusUpload | POST + PATCH + HEAD | /files/tus, then /files/tus/:session_id |
| getMeta | HEAD | /files/:id |
| delete | DELETE | /files/:id |
| signDownload | POST | /sign (JSON: file_id, exp_unix, perm) |
The Rust service routes are defined in services/fileserver/src/interface/http/router.rs.
Types and errors
Schemas (Zod, exported as types):
FileRecord—id,ownerId,filename,mimeType,sizeBytes,contentHash,visibility,createdAt,metadata?UploadResult—id,hash,dedupedVisibility—'public' | 'private' | 'unlisted'
FileserverError — code: NotFound | Forbidden | Conflict | Validation | Network | Unknown, plus optional HTTP status.
Extensibility
Implement FileserverGateway for mocks or alternate transports:
import type { FileserverGateway } from '@betechspark/fileserver-client';
const mockGateway: FileserverGateway = {
upload: async () => ({ id: '…', hash: '…', deduped: false }),
// …
};createHttpFileserverGateway accepts an optional fetchImpl (e.g. for Node undici or custom proxies).
Signing downloads
gateway.signDownload(fileId, expUnix, perm?) calls the File Server POST /sign endpoint. In production, prefer signing on the server with @betechspark/fileserver-server so the HMAC secret never reaches the browser.
Exports
- Domain —
FileserverError,FileRecordSchema,UploadResultSchema,VisibilitySchema, typesFileRecord,UploadResult,Visibility - Ports —
FileserverGateway,UploadInput,TusUploadInput - Use cases —
uploadFile,tusUpload,getFileMeta,deleteFile - Infrastructure —
createHttpFileserverGateway,HttpFileserverGateway,HttpFileserverGatewayOptions
Development
cd packages/fileserver-client
pnpm build # tsc → dist/
pnpm dev # tsc --watch
pnpm typecheck
pnpm cleanRelated
@betechspark/fileserver-react— React Query hooks and UI helpers@betechspark/fileserver-server— server-side HMAC URL signing- betechspark File Server — Rust service (default port 3004)
- Monorepo demos:
apps/example-next-file-server,apps/example-next-file-image
