Skip to content

Web UI & components

Basalt gives you two ways to put a screen in front of your API:

  1. Self-contained HTML pages — drop-in, build-free pages (teamsUiRoutes, billingUiRoutes, apiKeysUiRoutes, the audit viewer). No frontend project, no npm install on the client. Great for internal/admin screens.
  2. A real React frontend — a Vite + React app that talks to your API through a type-safe SDK and renders admin tables/forms from your Zod schemas. This is what create-basalt --ui scaffolds, and what this guide covers.

The --ui scaffold

bash
pnpm create basalt my-app --ui          # add --auth for the ready-made auth flows

Alongside the API, this generates a web/ frontend wired end to end:

web/
├── vite.config.ts     # dev server proxies /api → your backend (no CORS)
├── tailwind.config.js # Tailwind + shadcn theme, incl. @basaltkit/admin-shadcn in `content`
├── index.html
└── src/
    ├── api.ts         # your endpoints described once with @basaltkit/sdk
    ├── App.tsx        # the app — with --auth: login, register, forgot/reset, a dashboard + MFA
    ├── main.tsx
    └── index.css      # shadcn theme variables (light/dark)
  • React + Vite for the dev server and build.
  • @basaltkit/admin-shadcn — authentic shadcn/ui components, already themed.
  • @basaltkit/sdk — the type-safe client to your API.
  • The Vite dev server proxies /api to the backend, so the browser talks same-origin — no CORS to configure.

web is registered as a pnpm workspace member, so pnpm install at the root resolves it. Run the backend and pnpm --filter web dev (or the root dev script) and open the printed URL.

With --auth, App.tsx ships the full standard flows out of the box: sign in (with a TOTP challenge), register, forgot-password, reset-password via the emailed ?token link, and a dashboard that manages two-factor (enroll → secret/otpauth → activate → recovery codes → disable).

The type-safe SDK

@basaltkit/sdk is a drift-free HTTP client: describe each endpoint once with Zod, and every call gets the right input/output types, structured errors, and automatic token refresh. Its only dependency is Zod — it's browser-friendly and doesn't pull in @basaltkit/core.

ts
// src/api.ts — the single source of truth, shareable between client and tests
import { z } from 'zod'
import { endpoint } from '@basaltkit/sdk'

const Project = z.object({ id: z.string(), name: z.string() })

export const api = {
  projects: {
    list:   endpoint({ method: 'GET',  path: '/projects', result: z.array(Project) }),
    get:    endpoint({ method: 'GET',  path: '/projects/:id', params: z.object({ id: z.string() }), result: Project }),
    create: endpoint({ method: 'POST', path: '/projects', body: z.object({ name: z.string() }), result: Project }),
  },
}
ts
import { createClient } from '@basaltkit/sdk'
import { api } from './api.js'

const client = createClient(api, { baseUrl: '/api' })

const created = await client.projects.create({ body: { name: 'Basalt' } }) // typed { id, name }
const one     = await client.projects.get({ params: { id: created.id } })
const all     = await client.projects.list()

The client mirrors the shape of your api object, TypeScript checks the arguments, and the server's response is validated against the schema at runtime — a mismatch throws CLIENT_RESPONSE_MISMATCH instead of silently returning wrong data. Change a field on the backend and the frontend fails to compile, not in production.

Auth & token refresh

Pass a token and a refresh callback to createClient. On a 401 the client calls refresh once and retries with the new token — transparent to the caller. The --ui --auth scaffold wires this to the auth routes for you.

Admin panels from your Zod schemas

Three packages layer up so you can swap the visual look without rewriting logic:

PackageRole
@basaltkit/adminHeadless engine — from a Zod schema, derives table columns, form fields, and validation. Renders nothing.
@basaltkit/admin-reactReact layerDataTable, ResourceForm, useList, in plain unstyled HTML.
@basaltkit/admin-shadcnThe same components, styled with shadcn/ui (Tailwind). Identical props. What --ui uses.

1. Define the resource once

ts
// resources.ts — pure logic, no React
import { z } from 'zod'
import { defineResource } from '@basaltkit/admin'

export const projects = defineResource({
  name: 'projects',
  schema: z.object({
    id: z.string(),
    name: z.string(),
    status: z.enum(['draft', 'published']),
    archived: z.boolean().optional(),
  }),
  createSchema: z.object({
    name: z.string().min(3),
    status: z.enum(['draft', 'published']),
  }),
  columns: ['name', 'status'], // order shown in the table
})

From this, the engine derives the column labels, the form fields (with the right input per type — text, checkbox for booleans, <select> for enums, number for numbers), which fields are required, and the validation rules. Use fieldsFromSchema directly if you want the field models without a full resource.

2. Wire a data source

The engine reads and writes through an AdminDataSource{ list, get, create, update, remove }. Use memoryDataSource(seed) for demos, or back it with your type-safe SDK client for a real API:

ts
// source.ts
import type { AdminDataSource } from '@basaltkit/admin'
import { createClient } from '@basaltkit/sdk'
import { api } from './api'

const client = createClient(api, { baseUrl: '/api' })

export const projectsSource: AdminDataSource = {
  list:   ()          => client.projects.list(),
  get:    (id)        => client.projects.get({ params: { id } }),
  create: (input)     => client.projects.create({ body: input as { name: string } }),
  update: (id, input) => client.projects.update({ params: { id }, body: input }),
  remove: (id)        => client.projects.remove({ params: { id } }).then(() => true),
}

3. Render it

tsx
// ProjectsPage.tsx
import { DataTable, ResourceForm, useList } from '@basaltkit/admin-shadcn' // or @basaltkit/admin-react
import { projects } from './resources'
import { projectsSource } from './source'

export function ProjectsPage() {
  const { data, loading, error, reload } = useList(projectsSource)
  if (loading) return <p>Loading…</p>
  if (error) return <p>Something went wrong.</p>

  return (
    <>
      <DataTable resource={projects} rows={data} />
      <ResourceForm
        resource={projects}
        onSubmit={async (values) => {
          await projectsSource.create(values)
          reload()
        }}
      />
    </>
  )
}

useList(source) loads the list on mount and hands back { data, loading, error, reload }. DataTable formats cells (booleans as Yes/No, dates as 2026-08-07); ResourceForm renders one input per field with per-field validation and error messages driven by your createSchema. Swap the import between @basaltkit/admin-react (unstyled) and @basaltkit/admin-shadcn (styled) — the props are identical.

The shadcn primitives

@basaltkit/admin-shadcn also exports the shadcn primitives themselves — Button, Input, Label, Card, CardHeader, CardContent, CardTitle, Badge, Table — so you build the rest of your panel (headers, metric cards, actions) with the same look, without copying shadcn's files into your project.

Tailwind is required for styling

@basaltkit/admin-shadcn's classes only produce colors/spacing if your app has Tailwind CSS configured with shadcn's theme variables (--primary, --border, …) and includes the package in Tailwind's content:

js
// tailwind.config.js
content: ['./index.html', './src/**/*.{ts,tsx}', './node_modules/@basaltkit/admin-shadcn/dist/**/*.js']

The --ui scaffold does all of this for you. Integrating by hand? Follow ui.shadcn.com/docs/installation plus the content line above.

Dashboards

@basaltkit/dashboard composes an overview from your data — defineDashboard with metric/audit/queue sections, billing metrics (computeBillingMetrics, churnRate), and queue summaries — rendered by the same shadcn components.

Which UI approach?

  • Self-contained HTML pages (admin-pages) — no frontend project; mount a route and open the URL. Best for internal admin screens (API keys, team, billing, audit).
  • The React frontend (--ui) — a full SPA with the type-safe SDK and shadcn components, for the app your customers use.

They compose: a React app can still embed or link to the self-contained pages.

Released under the MIT License.