VOLT
Open Source Ecosystem

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/voltclient

Quick 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

PresetDescription
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

AdapterDependencyEnvironment
FetchHttpClientNone (native fetch)Browser, Node.js 18+
AxiosHttpClientaxios >= 1.0.0Browser, 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 a SingleClientConfig / MultiClientConfig object.
  • methods — a map of endpoint descriptors built with the helpers below. Path templates use ':param' placeholders (e.g. '/:id').
  • factory — a ClientFactory, (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

BuilderPurpose
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);
    }
  }
}

On this page