API Server & Code Generation
Workloom's backend is a standalone Hono API server with OpenAPI documentation generated from Zod schemas. A Kubb pipeline generates typed clients, React Query hooks, and Zod validators from the spec.
Architecture
┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐
│ apps/web │ │ apps/cli │ │ apps/api │
│ Next.js UI │ │ Commander │ │ Hono + zod-openapi │
│ (no API │ │ │ │ ├─ /api/v1/packages │
│ routes) │ │ │ │ ├─ /api/v1/workspaces │
└──────┬───────┘ └──────┬───────┘ │ ├─ /api/v1/objects │
│ │ │ ├─ /api/v1/admin │
│ │ │ ├─ /api/v1/telemetry │
▼ ▼ │ └─ /doc (OpenAPI JSON) │
┌──────────────────────────────────┐ └──────────────────────────┘
│ packages/workloom-api │ │
│ @workloom/workloom-api │◄────────────────┘
│ ├─ src/types/ (generated) │ kubb reads
│ ├─ src/client/ (generated) │ openapi.json
│ ├─ src/hooks/ (generated) │
│ └─ src/zod/ (generated) │
└──────────┬───────────────────────┘
│ imports
▼
┌──────────────────────┐ ┌──────────────────────┐
│ packages/fetch-client │ │ packages/react-query │
│ @workloom/fetch-client│ │ @workloom/react-query │
│ (auth, errors, fetch) │ │ (re-export + utils) │
└──────────────────────┘ └──────────────────────┘
Hono API Server (apps/api)
The API server uses @hono/zod-openapi to define routes with full OpenAPI documentation derived from Zod schemas.
Route Structure
apps/api/src/
├── index.ts # Server entry (serves on API_PORT, default 4040)
├── app.ts # createApp() factory
├── env.ts # Environment loading via @workloom/config
├── middleware/
│ ├── auth.ts # Session/token auth
│ ├── org-context.ts # Org resolution
│ ├── error-handler.ts # Global error → JSON
│ └── cors.ts # CORS for web app
├── routes/
│ ├── packages.ts # /api/v1/packages/*
│ ├── workspaces.ts # /api/v1/workspaces/*
│ ├── objects.ts # /api/v1/objects/*
│ ├── assets.ts # /api/v1/assets/*
│ ├── audit-logs.ts # /api/v1/audit-logs
│ ├── org.ts # /api/v1/org/*
│ ├── admin.ts # /api/v1/admin/*
│ ├── telemetry.ts # /api/v1/telemetry/*
│ └── auth.ts # /api/auth/*
├── schemas/ # Shared Zod schemas (request/response)
└── lib/ # DB, storage, audit helpers
Route Definition Pattern
Routes are defined declaratively using createRoute with Zod schemas that generate OpenAPI documentation:
import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
const listPackages = createRoute({
method: 'get',
path: '/api/v1/packages',
tags: ['Packages'],
summary: 'List packages',
request: {
query: z.object({
cursor: z.string().optional(),
limit: z.coerce.number().int().min(1).max(100).default(50),
scope: z.string().optional(),
}),
},
responses: {
200: {
description: 'Package list',
content: { 'application/json': { schema: PackageListResponseSchema } },
},
401: { description: 'Unauthorized' },
},
});
app.openapi(listPackages, async (c) => {
const { cursor, limit, scope } = c.req.valid('query');
// ... handler logic
return c.json({ data: packages, nextCursor, hasMore }, 200);
});
OpenAPI Spec Generation
The spec is generated at build time by extracting it from the app:
pnpm --filter @workloom/api generate:spec
This writes apps/api/openapi.json, which Kubb reads as input.
Authentication
The API supports two auth mechanisms:
| Method | Use Case |
|---|---|
| Bearer token (from OIDC session) | Browser requests proxied from web app |
WORKLOOM_DEV_TOKEN header | CLI development access |
Auth middleware (apps/api/src/middleware/auth.ts) validates tokens and injects the authenticated user into the Hono context.
Kubb Code Generation (packages/workloom-api)
Kubb v4 reads the OpenAPI spec and generates:
| Output | Directory | What It Generates |
|---|---|---|
| TypeScript types | src/types/ | Request/response interfaces |
| Fetch client | src/client/ | Typed fetch functions per endpoint |
| React Query hooks | src/hooks/ | useGet*, usePost*, usePatch* hooks |
| Zod schemas | src/zod/ | Runtime validation schemas |
Running Generation
# Generate OpenAPI spec from the API server
pnpm --filter @workloom/api generate:spec
# Generate client code from the spec
pnpm --filter @workloom/workloom-api generate
Kubb Configuration
Key configuration choices in packages/workloom-api/kubb.config.ts:
- Client:
fetch(native, no axios/xior dependency) - Import path:
@workloom/fetch-client(custom wrapper) - Path params:
objectstyle (e.g.,{ scope, name }) - Mutations: POST, PUT, PATCH, DELETE
- Queries: GET only
Fetch Client (packages/fetch-client)
A lightweight fetch wrapper providing:
- Bearer token injection from global config
- Org ID header (
X-Org-Id) - Error normalization (
WorkloomFetchError) - Base URL configuration
- Type-safe request/response contracts
import { configure, workloomClient } from '@workloom/fetch-client';
// Configure once at app startup
configure({
baseURL: 'http://localhost:4040',
token: accessToken,
orgId: 'default',
});
Type Contracts
The client exposes three core types that Kubb's generated code depends on:
interface RequestConfig<TVariables = unknown> {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
url: string;
baseURL?: string;
data?: TVariables;
params?: Record<string, string | number | boolean | undefined>;
headers?: Record<string, string>;
signal?: AbortSignal;
}
interface ResponseConfig<TData> {
data: TData;
status: number;
headers: Headers;
}
type ResponseErrorConfig<TError = unknown> = WorkloomFetchError<TError>;
React Query Package (packages/react-query)
Centralizes all React Query imports across the monorepo:
// All monorepo code imports from here, not @tanstack/react-query directly
import { useQuery, useMutation, QueryClient } from '@workloom/react-query';
Provides:
- Re-export of
@tanstack/react-query WorkloomQueryProviderwith sensible defaults (30s stale time, 1 retry)- DevTools integration
Frontend Integration
The web app (apps/web) uses the generated hooks with explicit auth headers:
import { useAccessToken } from '@/hooks/use-access-token';
import { useGetApiV1Packages } from '@workloom/workloom-api';
function PackageList() {
const { accessToken, headers } = useAccessToken();
const { data } = useGetApiV1Packages({
client: { headers },
query: { enabled: accessToken !== null },
});
return <ul>{data?.data.map(pkg => <li key={pkg.id}>{pkg.name}</li>)}</ul>;
}
Development Workflow
In development, the web app proxies API requests to the standalone server:
Browser → localhost:3040 (Next.js) → proxy /api/* → localhost:4040 (Hono)
Both processes are started via the Procfile:
overmind start
# web: Next.js on WEB_PORT (default 3040)
# api: Hono on API_PORT (default 4040)
# cli: tsc --build --watch
Adding a New Endpoint
- Define the Zod schema in
apps/api/src/schemas/ - Create the route in
apps/api/src/routes/usingcreateRoute - Regenerate the spec:
pnpm --filter @workloom/api generate:spec - Regenerate client code:
pnpm --filter @workloom/workloom-api generate - Import and use the new hook in
apps/web
The schema change propagates automatically through the pipeline — no hand-written client code needed.