A framework-free admin panel for the Petstore v3 API — built with plain TypeScript, Vite, and Tailwind CSS v4. No React. No Vue. No virtual DOM.
npm install && npm run dev → http://localhost:5173
A fully featured CRUD admin panel — pets, orders, users — backed by a Javalin 7 + MongoDB implementation of the OpenAPI Petstore spec. The interesting part isn't what it does, it's how it's built.
Glassmorphism UI · Mobile-first · Runtime URL switching · ~4 KB of hand-written infrastructure
| Layer | Choice | Why |
|---|---|---|
| Bundler | Vite 5 + TypeScript (strict) | Fast HMR, native ESM, zero config |
| Styling | Tailwind CSS v4 via @tailwindcss/vite |
No config file, no PostCSS — just @import "tailwindcss" in one CSS file |
| Reactivity | Hand-rolled signal / effect |
80 lines. Same pull-based dependency-tracking model as SolidJS and Vue 3, without the framework |
| Routing | Hand-rolled hash router | 40 lines. #/pets, #/store, #/users — zero server config needed |
| HTTP | Hand-rolled typed fetch wrapper | Injects baseUrl + X-API-KEY on every call, returns { data, error } — never throws |
| Font | Inter via @fontsource/inter |
Bundled, no Google Fonts DNS lookup |
No React. No Vue. No Svelte. No component framework of any kind.
The entire reactivity layer lives in src/lib/signal.ts. It implements the same pull-based dependency-tracking model used by SolidJS and Vue's Composition API.
// A signal is a reactive value container
const count = signal(0)
// An effect re-runs automatically whenever any signal it reads changes.
// Dependency tracking is automatic — no dependency arrays.
effect(() => {
document.title = `Count: ${count.get()}`
})
count.set(1) // → document.title updates to "Count: 1"How it works: each signal.get() call during an active effect registers that signal as a dependency of that effect. When signal.set() is called, it snapshots its subscriber set and runs each one. computed() is just an effect that writes to a backing signal.
The whole system is ~80 lines with no magic, no proxies, no Object.defineProperty.
src/lib/router.ts is a hash-based SPA router. Pages implement a simple two-function interface:
export function mount(container: HTMLElement): void { /* render + wire events */ }
export function unmount(): void { /* abort fetches, close modals */ }The router calls unmount() on the current page before calling mount() on the next one. Navigation is just location.hash = '#/pets'. No server config needed — the server always returns index.html; the fragment never leaves the browser.
The backend uses Java long for IDs — values up to 2⁶³−1. JavaScript's JSON.parse silently mangles any integer above 2⁵³−1 (9007199254740991):
JSON.parse('{"id":7281654362742761859}')
// → { id: 7281654362742761000 } ← silent precision loss
The fix lives in src/lib/api.ts: before calling JSON.parse, a regex replaces 16+ digit integers in the raw response text with quoted strings.
const LARGE_INT_RE = /([:\[,])\s*(-?\d{16,})(?=[,\}\]\s])/g
function safeParseJson<T>(text: string): T {
return JSON.parse(text.replace(LARGE_INT_RE, '$1 "$2"')) as T
}7281654362742761859 becomes "7281654362742761859". All id fields in the TypeScript types are string. No precision loss, no external dependency.
The settings panel uses a frosted-glass backdrop behind the slide-out drawer. Early implementation:
// ❌ backdrop-blur on a fixed inset-0 overlay creates a compositing layer that
// intercepts pointer events — even when sibling elements have higher z-index.
// Reproducible in Chrome and Safari. Inputs inside the drawer become unclickable.
backdrop.className = 'fixed inset-0 z-20 bg-black/20 backdrop-blur-sm'
backdrop.addEventListener('click', close)Fix: remove backdrop-filter from the overlay, use pointer-events-none, and detect outside-clicks with a document listener instead.
// ✅ purely visual — pointer events pass through
backdrop.className = 'fixed inset-0 z-20 bg-black/20 pointer-events-none'
// outside-click-to-close via document, not the backdrop
document.addEventListener('click', (e) => {
if (isOpen && !drawer.contains(e.target as Node)) close()
})
triggerBtn.addEventListener('click', (e) => { e.stopPropagation(); toggle() })Tailwind v4 scans source files at build time to generate CSS. Classes added only via classList.add() in JavaScript are invisible to the scanner — they never make it into the bundle.
// ❌ translate-x-0 won't be in the generated CSS
drawer.classList.remove('translate-x-full')
drawer.classList.add('translate-x-0') // no-op at runtime// ✅ inline styles are always applied
drawer.style.transform = 'translateX(0)'This applies to every animated element in the project (sidebar, settings drawer). The transition-transform duration-300 class stays in the static class string so Tailwind generates it — only the transform value is set via JS.
Pets — full CRUD with filter tabs (available / pending / sold), photo upload via application/octet-stream, inline photoBase64 thumbnails, tag badges.
Store — live inventory summary cards (counts from GET /store/inventory), order management. Orders are tracked in sessionStorage — the Petstore API has no list-all endpoint.
Users — full CRUD, login/logout with session token display. Known usernames tracked in sessionStorage.
Settings panel — slide-out drawer with two backend URL presets (local dev + petstore3.swagger.io) plus a free-text custom input. API key field. Both persisted to localStorage and read as defaults from .env (VITE_BASE_URL, VITE_API_KEY).
Mobile — off-canvas sidebar (hamburger toggle), card-list layout instead of tables, full-screen modals, bottom-centre toasts.
Dark slate-green glassmorphism. No design library — every token is a Tailwind utility class.
Background from-[#060a06] via-[#0d1a0f] to-[#060a06]
Glass panel bg-white/10 backdrop-blur-md border border-white/15 rounded-2xl
Accent emerald-600 / emerald-500
Status badges use semi-transparent colour rings (ring-1 ring-emerald-500/30) over matching backgrounds (bg-emerald-500/20). The API returns uppercase status values (AVAILABLE); the badge component normalises with .toLowerCase() before switching.
petstore-sync — a Javalin 7 + MongoDB implementation of the OpenAPI Petstore v3 spec, running on port 7070. Live OpenAPI spec at http://localhost:7070/openapi.
export MONGO_URI="mongodb+srv://..."
export API_KEY="special-key"
java -jar target/petstore-sync-*-jar-with-dependencies.jarcp .env.example .env # set VITE_API_KEY and VITE_BASE_URL
npm install
npm run devFor contributors and AI assistants: see
CLAUDE.mdfor architecture details, file map, and known gotchas.