Type-Safe Client (Hive)
Nimbus Hive provides a Tuyau-equivalent type-safe HTTP client system for AdonisJS-like monorepos and standalone frontend projects. It lets you introspect your backend routes and validation schemas at compile-time to expose a fully typed proxy API client on the frontend.
Key Features
- End-to-End Type Safety: Autocomplete for routes, path parameters, query parameters, request bodies, and responses.
- Runtime Schema Reflection: Generates TypeScript interfaces from backend
validation.Schemavalidations automatically. - Zero-Throw API (.safe()): Return
[data, error]tuples instead of wrapping requests in messy try-catch blocks. - Dynamic Form Data Mapping: Automatically converts body payloads containing
FileorFile[]into standard Multi-part FormData.
Step 1: Mark Routes with Schema and Response Hints
In your backend start/routes.go, register the validator schema on the route using the .Schema() builder, and declare the expected response structure using .Response().
package start
import (
"github.com/CodeSyncr/nimbus"
"github.com/CodeSyncr/nimbus/validation"
)
var postSchema = validation.Schema{
"title": validation.String().Required().Min(3).Max(255),
"content": validation.String().Required(),
"category_id": validation.Number().Required(),
"published": validation.Bool(),
}
type PostResponse struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
}
func RegisterRoutes(app *nimbus.App) {
// Register route and append the schema and response metadata
app.Router.Post("/api/posts", storePostHandler).
As("posts.store").
Schema(postSchema).
Response(&PostResponse{})
app.Router.Get("/api/posts/:id", showPostHandler).
As("posts.show").
Response(&PostResponse{})
}
Step 2: Generate the Route Manifest
To produce the route manifest registry and parameter/response types, run the code generator command in your project root:
# Run your server with NIMBUS_DUMP_ROUTES=1 to emit .route-manifest.json
NIMBUS_DUMP_ROUTES=1 go run .
# Run the client code-generator command to produce the TypeScript files
nimbus gen:client
This writes a TypeScript module to .nimbus-client/registry.ts detailing your API surface, and a .nimbus-client/data.d.ts containing the types of your validation schemas and response structures:
// Auto-generated by `nimbus gen:client` โ DO NOT EDIT
import type * as Data from './data.js'
export const registry = {
"posts.show": {
method: "GET" as const,
path: "/api/posts/:id",
params: { "id": "string" } as Record<string, 'string'>,
types: {} as {
params: { id: string };
query: Record<string, never>;
body: Record<string, never>;
response: Data.PostsShowResponse;
}
},
"posts.store": {
method: "POST" as const,
path: "/api/posts",
params: {} as Record<string, never>,
types: {} as {
params: Record<string, never>;
query: Record<string, never>;
body: Data.PostsStoreBody;
response: Data.PostsStoreResponse;
}
}
} as const
export type RouteName = keyof typeof registry
Step 3: Setup the Frontend API Client
Install the Hive TypeScript package to your frontend project:
npm install @codesyncr/hive
Create an api.ts file defining your client. Because the types for request bodies and path parameters are embedded directly in the generated registry object, the client automatically yields complete type safety with zero manual type annotations required:
import { createHive } from '@codesyncr/hive'
import { registry } from '../../.nimbus-client/registry'
// Instantiate the proxy client
export const client = createHive({
baseUrl: 'http://localhost:8080',
registry,
credentials: 'include', // sends session cookies automatically
})
Monorepo Setup & Package Exports
If you are running a monorepo setup (e.g. backend and frontend in different packages), you can configure your Go backend's package.json to export the generated client files directly:
{
"name": "@my-app/backend",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
"./registry": "./.nimbus-client/registry.ts",
"./data": "./.nimbus-client/data.d.ts"
}
}
This allows your React, Next.js, Vue, or Svelte frontend application to import the registry cleanly without deep relative paths:
import { createHive } from '@codesyncr/hive'
import { registry } from '@my-app/backend/registry'
export const client = createHive({
baseUrl: import.meta.env.VITE_API_URL || 'http://localhost:8080',
registry,
})
Step 4: Make Type-Safe Calls
Interact with your API endpoints with full TypeScript autocomplete support.
import { client } from './api'
// 1. Fluent Proxy Call (Tuyau style)
const post = await client.api.posts.store({
body: {
title: 'Introducing Hive',
content: 'A type-safe client built for speed.',
category_id: 1,
}
})
console.log(post.id) // Typesafe autocomplete
// 2. Direct Named Call (String-based option)
const single = await client.$('posts.show', {
params: { id: '42' }
})
Error Handling: The .safe() Modifier
By default, requests throw errors on HTTP statuses outside the 2xx range or on network failures. Appending .safe() returns a [data, error] tuple that never throws.
const [data, error] = await client.api.posts.show({
params: { id: '9999' }
}).safe()
if (error) {
if (error.isStatus(404)) {
console.error('Post not found:', error.response.error)
} else if (error.isValidationError()) {
console.error('Validation error list:', error.response.errors)
} else {
console.error('Unexpected error:', error.message)
}
return
}
console.log('Post retrieved:', data.title)
URL & Route Utilities
Generate client URLs safely using route names and parameters:
// Generates "/api/posts/42?ref=docs"
const path = client.url('posts.show', { id: '42' }, { ref: 'docs' })
// Introspect active route (Browser only)
if (client.current('posts.*')) {
console.log('User is reading some post page')
}
Resilience: Retries, Timeouts & Cancellation
Configure automatic retries on the client. Retries fire on network errors and configured status codes โ only for idempotent methods โ using exponential backoff with jitter (capped at backoffLimit) and honoring a Retry-After response header.
export const client = createHive({
baseUrl: 'http://localhost:8080',
registry,
timeout: 15000, // per-request timeout in ms (default 30000)
retry: {
limit: 3,
methods: ['get', 'put', 'head', 'delete', 'options', 'trace'], // default (idempotent)
statusCodes: [408, 413, 429, 500, 502, 503, 504], // default
backoffLimit: 30000,
onRetry: ({ attempt, error, delay }) =>
console.warn(`retry ${attempt} in ${delay}ms`, error.message),
},
})
Override per call โ timeout, signal (caller cancellation, combined with the timeout signal), and retry. Array query values serialize as repeated params.
const ac = new AbortController()
const posts = await client.api.posts.index({
query: { tag: ['go', 'web'], page: 2 }, // โ ?tag=go&tag=web&page=2
timeout: 5000,
signal: ac.signal,
retry: { limit: 0 }, // disable retries for this call
})
// ac.abort() cancels the in-flight request.
Note: POST is not retried by default (non-idempotent) โ add 'post' to retry.methods only if the endpoint is safe to repeat.
See Also
- Backend Routing โ Defining paths and route groups
- Validation Schemas โ Writing robust validation rules
- OpenAPI Integration โ Generating standard OpenAPI specs