Skip to main content

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:

MethodUse Case
Bearer token (from OIDC session)Browser requests proxied from web app
WORKLOOM_DEV_TOKEN headerCLI 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:

OutputDirectoryWhat It Generates
TypeScript typessrc/types/Request/response interfaces
Fetch clientsrc/client/Typed fetch functions per endpoint
React Query hookssrc/hooks/useGet*, usePost*, usePatch* hooks
Zod schemassrc/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: object style (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
  • WorkloomQueryProvider with 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

  1. Define the Zod schema in apps/api/src/schemas/
  2. Create the route in apps/api/src/routes/ using createRoute
  3. Regenerate the spec: pnpm --filter @workloom/api generate:spec
  4. Regenerate client code: pnpm --filter @workloom/workloom-api generate
  5. Import and use the new hook in apps/web

The schema change propagates automatically through the pipeline — no hand-written client code needed.