VoltClient (Node.js)
TypeScript HTTP client for the VOLT REST API.
Overview
@voltstack/voltclient works in both browser and Node.js environments and provides typed response unwrapping, in-flight GET deduplication, RBAC team scoping, and paginated responses.
Installation
npm install @voltstack/voltclientQuick Start
import { createVoltClient, secretKey } from '@voltstack/voltclient';
const volt = createVoltClient('https://api.voltcloud.dev/api', {
credential: secretKey('vsk_...'),
});
// Scope requests to a specific team
const team = volt.withTeam('team-id');
// List trajectories
const trajectories = await team.getUnwrapped('/trajectory');Factory Function
createVoltClient(baseUrl, options?)
const volt = createVoltClient('https://api.voltcloud.dev/api', {
credential: secretKey('vsk_...'),
adapter: 'fetch',
teamId: 'team-id',
});interface VoltClientFactoryOptions {
// Credential provider for authentication. If omitted, requests are
// sent without an Authorization header.
credential?: CredentialProvider;
// HTTP adapter to use. 'fetch' (default) or 'axios'.
adapter?: 'fetch' | 'axios';
// Request timeout in milliseconds. Default: 30000.
timeout?: number;
// Global RBAC team ID scope, injected into every request URL.
teamId?: string;
}Team scoping is configured via the teamId option or the withTeam() method — there is no credentials field.
Authentication Presets
| Preset | Description |
|---|---|
staticToken(token) | Fixed Bearer token |
secretKey(key) | API secret key as Bearer token |
dynamicToken(fn) | Async function that returns a token on each request |
import { staticToken, secretKey, dynamicToken } from '@voltstack/voltclient';
// Static token
const creds = staticToken('eyJhbGciOi...');
// Secret key (VoltLabs keys use a 'vsk_' prefix)
const creds = secretKey('vsk_...');
// Dynamic token (e.g., from a refresh flow)
const creds = dynamicToken(async () => {
const { token } = await refreshToken();
return token;
});HTTP Adapters
| Adapter | Dependency | Environment |
|---|---|---|
FetchHttpClient | None (native fetch) | Browser, Node.js 18+ |
AxiosHttpClient | axios >= 1.0.0 | Browser, Node.js |
Both adapters require a baseUrl and accept an optional credential (singular) and timeout.
import { VoltClient, FetchHttpClient, AxiosHttpClient } from '@voltstack/voltclient';
// Using fetch (default)
const http = new FetchHttpClient({
baseUrl: 'https://api.voltcloud.dev/api',
credential: secretKey('vsk_...'),
});
// Using axios
const http = new AxiosHttpClient({
baseUrl: 'https://api.voltcloud.dev/api',
credential: secretKey('vsk_...'),
});Constructor
class VoltClient {
constructor(
http: HttpClient,
basePath: string,
opts?: VoltClientOptions
)
}Prefer createVoltClient() over calling the constructor directly.
Methods
Team Scoping
withTeam(teamId: string): VoltClient
Returns a new client; subsequent requests include the team ID in the URL path for RBAC resolution.
const teamClient = volt.withTeam('abc123');withBasePath(basePath: string, opts?: VoltClientOptions): VoltClient
Returns a new client scoped to a sub-path, reusing the same HTTP adapter.
const trajectoryClient = volt.withBasePath('/trajectory');Basic Requests
get<T>(path, query?): Promise<T>
Identical concurrent GET requests are deduplicated — the same promise is shared.
const data = await volt.get('/trajectory', { page: 1, limit: 10 });post<T>(path, body?): Promise<T>
const analysis = await volt.post('/analysis', { trajectoryId, pluginId });patch<T>(path, body?): Promise<T>
await volt.patch('/trajectory/abc123', { name: 'Updated Name' });delete<T>(path, query?): Promise<T>
await volt.delete('/trajectory/abc123');Envelope Unwrappers
The VOLT API wraps responses in a { status, data } envelope; these methods extract the inner data.
getUnwrapped<T>(path, query?): Promise<T>
// Instead of: const { data } = await volt.get('/trajectory');
const trajectories = await volt.getUnwrapped('/trajectory');postUnwrapped<T>(path, body?): Promise<T>
patchUnwrapped<T>(path, body?): Promise<T>
deleteUnwrapped<T>(path, query?): Promise<T>
Field Extractors
Extract a specific field from the unwrapped response.
getField<T, K>(path, field, query?): Promise<T[K]>
const count = await volt.getField('/trajectory/count', 'count');postField<T, K>(path, field, body?): Promise<T[K]>
patchField<T, K>(path, field, body?): Promise<T[K]>
Pagination
getPaginated<T>(path, params?): Promise<PaginatedResponse<T>>
Returns a normalized paginated response.
interface PaginatedResponse<T> {
status: 'success';
data: T[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
hasMore: boolean;
};
_meta?: Record<string, unknown>;
}const page = await volt.getPaginated('/trajectory', { page: 1, limit: 20 });
console.log(page.data, page.pagination.totalPages);File Downloads
exportFile(path, params?): Promise<Blob>
const blob = await volt.exportFile('/analysis/abc123/export');Service DSL
createService(config, methods, factory):
config— a base-path string (e.g.'/trajectory'), or aSingleClientConfig/MultiClientConfigobject.methods— a map of endpoint descriptors built with the helpers below. Path templates use':param'placeholders (e.g.'/:id').factory— aClientFactory,(basePath, opts?) => VoltClient, that creates the scoped clients.
Methods on the returned service are invoked directly — service.method(params) — with no second application.
import { createService, get, post, patch, del, paginated } from '@voltstack/voltclient';
const clientFactory = (basePath: string) => volt.withBasePath(basePath);
const TrajectoryService = createService(
'/trajectory',
{
list: paginated<{ page: number }, Trajectory>('/'),
getById: get<{ id: string }, Trajectory>('/:id'),
create: post<Trajectory, Trajectory>('/'),
update: patch<{ id: string } & Partial<Trajectory>, Trajectory>('/:id'),
remove: del<{ id: string }>('/:id'),
},
clientFactory,
);
// Usage — methods are invoked directly on the built service
const trajectories = await TrajectoryService.list({ page: 1 });
const trajectory = await TrajectoryService.getById({ id: 'abc123' });DSL builders
| Builder | Purpose |
|---|---|
get(path, opts?) | GET endpoint |
post(path, opts?) | POST endpoint |
patch(path, opts?) | PATCH endpoint |
del(path, opts?) | DELETE endpoint |
paginated(path, opts?) | GET endpoint normalized to PaginatedResponse<T> |
request(method, path, opts?) | Raw HTTP request with an explicit method |
download(method, path, opts?) | Returns a Blob (auto-sets responseType: 'blob' and unwrap: 'raw') |
custom(run, opts?) | Full control over execution via a run callback |
Error Handling
ApiError exposes a string code (e.g. 'Network::Timeout', 'Auth::Unauthorized') as its primary discriminator, alongside an optional HTTP status and message. It also provides getFriendlyMessage(), isPermissionDenied(), and the static ApiError.isRBACError(err) helper.
import { ApiError } from '@voltstack/voltclient';
try {
await volt.get('/trajectory/invalid');
} catch (err) {
if (err instanceof ApiError) {
if (ApiError.isRBACError(err)) {
console.error('Permission denied:', err.getFriendlyMessage());
} else {
console.error(err.code, err.status, err.message);
}
}
}