Skip to content

Hono RPC Client — Typed API calls between Angular and Worker

Hono RPC Client — Typed API calls between Angular and Worker

Overview

The Bloqr Compiler uses hono/client to provide end-to-end type-safe HTTP calls from the Angular frontend to the Cloudflare Worker.

Two services cover the full API surface:

ServiceFileScope
ApiClientServicefrontend/src/app/services/api-client.tsPublic (unauthenticated) endpoints
AuthedApiClientServicefrontend/src/app/services/authed-api-client.service.tsAuthenticated endpoints (Bearer + Trace-ID)
sequenceDiagram
    participant A as Angular Component
    participant P as ApiClientService (public)
    participant Q as AuthedApiClientService (authed)
    participant H as hc<AppType>()
    participant W as Cloudflare Worker (Hono)

    A->>P: inject(ApiClientService)
    P->>H: hc<AppType>(baseUrl)
    A->>H: client.api.health.$get()
    H->>W: GET /api/health (no auth)
    W-->>A: typed JSON

    A->>Q: inject(AuthedApiClientService)
    Q->>Q: getHeaders() → Bearer + X-Trace-ID
    A->>Q: compile(request)
    Q->>W: POST /api/compile  Authorization: Bearer …  X-Trace-ID: …
    W-->>A: typed JSON

Architecture

LayerFileRole
Workerworker/hono-app.tsExports AppType = typeof app
Shared TypesAppType in api-client.tsRequest/response types for all routes
Public clientfrontend/src/app/services/api-client.tsApiClientService — unauthenticated endpoints
Authed clientfrontend/src/app/services/authed-api-client.service.tsAuthedApiClientService — authenticated endpoints
ComponentsAny component that needs typed API callsInjects one or both services

Worker: Exporting AppType

The worker exports the app’s type so the frontend can mirror it:

worker/hono-app.ts
export const app = new OpenAPIHono<{ Bindings: Env; Variables: Variables }>();
// ... route definitions ...
export type AppType = typeof app;

Angular: Public RPC Client (ApiClientService)

The ApiClientService in frontend/src/app/services/api-client.ts wraps the hc<AppType>() call with Angular’s dependency injection:

import { ApiClientService } from './services/api-client';
@Component({ standalone: true, ... })
export class HealthComponent {
private readonly apiClient = inject(ApiClientService);
async checkHealth(): Promise<void> {
// Fully typed request and response — no manual interface needed
const res = await this.apiClient.client.api.health.$get();
if (res.ok) {
const data = await res.json();
console.log(data.status); // 'healthy' | 'degraded' | 'down'
console.log(data.version); // string
console.log(data.timestamp); // string
}
}
}

OpenAPI spec endpoint

// Fetch the live OpenAPI spec document (public, no auth required)
const res = await this.apiClient.client.api['openapi.json'].$get();
const spec = await res.json();

Angular: Authenticated RPC Client (AuthedApiClientService)

The AuthedApiClientService uses the same hc<AppType>() pattern but injects a Bearer token and trace ID header before every call. Tokens are resolved via AuthFacadeService.getToken() (reads the Better Auth session cookie) — they are never stored in component state or localStorage.

Usage

import { AuthedApiClientService } from './services/authed-api-client.service';
@Component({ standalone: true, ... })
export class CompileComponent {
private readonly rpc = inject(AuthedApiClientService);
async runCompile(): Promise<void> {
// Bearer token + X-Trace-ID are injected automatically
const result = await this.rpc.compile({
configuration: {
name: 'My List',
sources: [{ source: 'https://easylist.to/easylist/easylist.txt' }],
transformations: ['RemoveComments', 'Deduplicate'],
},
});
console.log(result.ruleCount); // number
}
}

Available methods

MethodHTTPPathTier required
compile(request)POST/api/compileFree
validateRules(request)POST/api/validateFree
validateRule(request)POST/api/validate-ruleFree
listRules()GET/api/rulesFree
createRuleSet(request)POST/api/rulesFree
compileAsync(request)POST/api/compile/asyncPro

getHeaders() internals

private async getHeaders(): Promise<Record<string, string>> {
const headers: Record<string, string> = {
'X-Trace-ID': this.log.sessionId,
};
if (!this.auth.isSignedIn()) return headers;
const token = await this.auth.getToken();
if (!token) throw new Error('Session token unavailable — please sign in again');
headers['Authorization'] = `Bearer ${token}`;
return headers;
}

ZTA compliance

  • Token resolved per-call (not cached in the service).
  • Throws with a descriptive message on auth failure.
  • X-Trace-ID always set for Worker-side log correlation.
  • Never touches localStorage or component state.

When to use AuthedApiClientService vs HttpClient services

ScenarioRecommended service
Components in the browser + Angular HTTP lifecycleEither — AuthedApiClientService is simpler
SSR / service workers / outside Angular DIAuthedApiClientService (no HttpClient dep)
Streaming / WebSocket upgradesHttpClient with reportProgress: true
Batch observables / reactive pipelinesHttpClient + RxJS (CompilerService)

AppType

AppType in api-client.ts covers all routes — both public and authenticated. This single type is shared by both ApiClientService and AuthedApiClientService.

export type AppType = {
api: {
// Public routes
health: { $get: () => TypedResponse<HealthResponse> };
version: { $get: () => TypedResponse<VersionResponse> };
'openapi.json': { $get: () => TypedResponse<Record<string, unknown>> };
// Authenticated routes
compile: { $post: (opts: { json: CompileRequest }) => TypedResponse<CompileResponse> };
validate: { $post: (opts: { json: ValidateRequest }) => TypedResponse<ValidateResponse> };
'validate-rule': { $post: (opts: { json: ValidateRuleRequest }) => TypedResponse<ValidateRuleResponse> };
rules: {
$get: () => TypedResponse<RulesListData>;
$post: (opts: { json: RuleSetCreate }) => TypedResponse<{ success: boolean; ruleSet: RuleSetData }>;
};
};
};

AppType evolution

To replace the inline AppType with the real worker type:

// Option A: Direct cross-workspace path import (works during local dev)
import type { AppType } from '../../../../worker/hono-app';
// Option B: Published types package (recommended for production)
import type { AppType } from '@bloqr-backend/worker-types';

To share types across the monorepo without a published package, add a paths mapping to the Angular tsconfig.json:

{
"compilerOptions": {
"paths": {
"@bloqr-backend/worker/*": ["../worker/*"]
}
}
}

Server-Timing headers

The Hono worker adds Server-Timing headers to every response (via hono/timing).

For HttpClient-based services:

const res = await this.http.post('/api/compile', body, { observe: 'response' }).toPromise();
const timing = res?.headers.get('Server-Timing');
// "auth;dur=12.3, handler;dur=245.7"

For AuthedApiClientService, use the raw ClientResponse:

const rawRes = await this.rpcClient.api.compile.$post({ json: body }, { headers });
const timing = rawRes.headers.get('Server-Timing');

Route Coverage

Public routes (ApiClientService)

MethodPathClient method
GET/api/healthclient.api.health.$get()
GET/api/versionclient.api.version.$get()
GET/api/openapi.jsonclient.api['openapi.json'].$get()

Authenticated routes (AuthedApiClientService)

MethodPathService methodTier
POST/api/compilerpc.compile(request)Free
POST/api/validaterpc.validateRules(request)Free
POST/api/validate-rulerpc.validateRule(request)Free
GET/api/rulesrpc.listRules()Free
POST/api/rulesrpc.createRuleSet(request)Free
POST/api/compile/asyncrpc.compileAsync(request)Pro

References