Skip to content

Building a Web UI

You have Strata services running. You have an RPC Bridge in front of them. Now you want your web app to call services from the browser.

This guide walks through using @strata-js/rpcbridge-client — a lightweight WebSocket client that connects to the bridge and gives you type-safe access to your services.

Install

bash
npm install @strata-js/rpcbridge-client

Basic Usage

typescript
import { WebSocketClient } from '@strata-js/rpcbridge-client';

const client = new WebSocketClient('http://localhost:3000/rpc');

// Type-safe request with generics
interface UserProfile
{
    id : string;
    name : string;
    email : string;
}

const user = await client.request<UserProfile>(
    'users', 'profile', 'get',
    { userId: 'abc123' }
);

console.log(user.name);

The client connects via Socket.IO, emits an rpc event with the request, and resolves the promise with the service's response payload.

Connection Lifecycle

The client uses Socket.IO under the hood, which handles reconnection automatically. The defaults are sensible for most cases:

typescript
const client = new WebSocketClient('http://localhost:3000/rpc', {
    transports: [ 'websocket' ],
    reconnection: true,
    emitTimeout: 20000,
});

Monitoring Connection State

Access the underlying Socket.IO instance for connection events:

typescript
const socket = client.socket;

socket.on('connect', () =>
{
    console.log('Connected to bridge');
});

socket.on('disconnect', (reason) =>
{
    console.log('Disconnected:', reason);
});

socket.on('connect_error', (err) =>
{
    console.error('Connection failed:', err.message);
});

Handling Disconnects in the UI

In a real app, you want the UI to react to connection state. A common pattern:

typescript
import { ref } from 'vue';

const connected = ref(false);

client.socket.on('connect', () => { connected.value = true; });
client.socket.on('disconnect', () => { connected.value = false; });

Then show a banner or disable actions when connected is false. Don't silently queue requests — users should know when the connection is down.

Building a Transport Layer

For anything beyond a prototype, wrap the WebSocketClient in an application-specific transport class. This gives you a single place to handle auth injection, metadata, and session management.

typescript
import { WebSocketClient, RemoteServiceError } from '@strata-js/rpcbridge-client';

class AppTransport
{
    private client : WebSocketClient;
    private getToken : () => string | undefined;

    constructor(bridgeUrl : string, getToken : () => string | undefined)
    {
        this.client = new WebSocketClient(bridgeUrl);
        this.getToken = getToken;
    }

    async request<T>(
        service : string,
        context : string,
        operation : string,
        payload : Record<string, unknown> = {}
    ) : Promise<T>
    {
        const token = this.getToken();
        return this.client.request<T>(
            service, context, operation,
            payload,
            undefined,
            token
        );
    }

    get socket() { return this.client.socket; }
}

Usage:

typescript
const transport = new AppTransport(
    'http://localhost:3000/rpc',
    () => authStore.accessToken
);

const user = await transport.request<UserProfile>('users', 'profile', 'get', { userId });

This keeps auth handling centralized. Every request automatically picks up the current token without callers needing to think about it.

Error Handling

The client throws two typed errors:

RemoteServiceError

The bridge received a response from the service, but the service returned a failure. Contains code, message, name, stack, and optional details from the remote service.

EmitTimeoutError

The Socket.IO emit timed out — the bridge didn't respond within the configured emitTimeout.

typescript
import { RemoteServiceError, EmitTimeoutError } from '@strata-js/rpcbridge-client';

try
{
    await client.request('users', 'profile', 'get', { userId: 'bad' });
}
catch(err)
{
    if(err instanceof RemoteServiceError)
    {
        // Service returned an error — show the user a meaningful message
        showError(`${ err.message } (${ err.code })`);
    }
    else if(err instanceof EmitTimeoutError)
    {
        // Bridge didn't respond — likely a connection issue
        showError('Request timed out. Check your connection.');
    }
}

In a transport layer, you can intercept specific error codes for cross-cutting concerns:

typescript
async request<T>(/* ... */) : Promise<T>
{
    try
    {
        return await this.client.request<T>(/* ... */);
    }
    catch(err)
    {
        if(err instanceof RemoteServiceError && err.code === 'SESSION_EXPIRED')
        {
            // Trigger a re-auth flow
            authStore.clearSession();
            router.push('/login');
        }
        throw err;
    }
}

Auth Integration

Passing Tokens

The auth parameter on request() is passed through to the Strata service as-is. What you put there depends on your auth setup:

typescript
// Bearer token
await client.request('users', 'profile', 'get', payload, undefined, `Bearer ${ token }`);

// Or via the transport layer (recommended)
const transport = new AppTransport(bridgeUrl, () => `Bearer ${ authStore.token }`);

Token Refresh

If your tokens expire, handle refresh in the transport layer:

typescript
async request<T>(/* ... */) : Promise<T>
{
    let token = this.getToken();

    // Check if token needs refresh before sending
    if(this.isTokenExpired(token))
    {
        token = await this.refreshToken();
    }

    return this.client.request<T>(service, context, operation, payload, undefined, token);
}

Session Expiry

When the remote service detects an invalid session, it will return a service error. Catch it in your transport layer and redirect to login:

typescript
if(err instanceof RemoteServiceError && err.code === 'UNAUTHORIZED')
{
    authStore.clearSession();
    router.push('/login');
}

Production Considerations

CORS

If your web app and bridge are on different origins, configure CORS on the bridge server:

typescript
import cors from 'cors';

// When using BYOS (Bring Your Own Server)
app.use(cors({ origin: 'https://app.example.com', credentials: true }));

For Socket.IO, pass CORS options to the server:

typescript
const io = new SIOServer(server, {
    cors: {
        origin: 'https://app.example.com',
        credentials: true,
    },
});

Path Configuration

Match the bridge's path config to your deployment:

typescript
// Bridge config
server: { path: '/api/rpc', /* ... */ }

// Client connection
const client = new WebSocketClient('https://app.example.com/api/rpc');

Enable Only What You Need

If your app only uses WebSocket, disable HTTP (and vice versa):

typescript
server: {
    enableHTTP: false,
    enableWS: true,
    // ...
}

Fewer endpoints = smaller attack surface.

Next Steps