.use() for mounting sub-appsnpm install spiceflow@rsc
spiceflow@rsc, not spiceflow@latest.npx -y skills add remorses/spiceflow -g -y
.md to any getspiceflow.com URL, for example https://getspiceflow.com/react-data.md..page() and .layout() for server-rendered UI with client interactivity:import { Spiceflow } from 'spiceflow' import { Counter } from './counter' export const app = new Spiceflow() .get('/api/hello', () => { return { message: 'Hello, World!' } }) .layout('/*', async ({ children }) => { return ( <html> <body>{children}</body> </html> ) }) .page('/', async () => { return ( <div> <h1>Home</h1> <Counter /> </div> ) }) .page('/about', async () => { return <h1>About</h1> }) app.listen(3000)
.route() instead of .get()/.post() when you want to pass Zod schemas for validation — it accepts request, response, query, and params schemas.spiceflow and build type-safe APIs with Zod validation, streaming, OpenAPI, and a type-safe fetch client:import { Spiceflow } from 'spiceflow' const app = new Spiceflow() .get('/hello', () => ({ message: 'Hello!' })) app.listen(3000)
// vite.config.ts import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' import spiceflow from 'spiceflow/vite' export default defineConfig({ plugins: [react(), spiceflow({ entry: './src/main.tsx' })], })
Spiceflow instance: API routes, middleware, loaders, layouts, and pages. The chain is the source of truth for both runtime routing and TypeScript inference — the typed fetch client, router.href(), and useLoaderData() all read their types from typeof app.new Spiceflow() ── one chain, one type │ ┌─────────────────────┼──────────────────────┐ v v v .get / .post .use(middleware) .loader / .layout / .page .route (Zod) serveStatic, cors React Server Components JSON + SSE APIs mounted sub-apps server actions, client chunks │ │ │ └─────────────────────┼──────────────────────┘ v app.listen(3000) / app.handle(request) Node.js <──────> Bun <──────> Cloudflare Workers
"use client" file automatically becomes its own browser chunk. Server actions marked "use server" become POST endpoints that re-render the page with fresh data after they run.// src/main.tsx import { Spiceflow, parseFormData } from 'spiceflow' import { Head, Link, router } from 'spiceflow/react' import { z } from 'zod' import { Counter } from './app/counter' export const app = new Spiceflow() .get('/api/hello', () => ({ message: 'Hello!' })) .loader('/dashboard/*', async ({ request }) => { const user = await getUser(request) return { user } }) .layout('/*', async ({ children }) => { return ( <html> <Head> <Head.Meta charSet="UTF-8" /> </Head> <body>{children}</body> </html> ) }) .page('/dashboard', async ({ loaderData }) => { return ( <div> <h1>Welcome {loaderData.user.name}</h1> <Counter /> <Link href={router.href('/')}>Home</Link> </div> ) }) .listen(3000) // Register the app type for type-safe routing everywhere declare module 'spiceflow/react' { interface SpiceflowRegister { app: typeof app } }
.staticPage() renders a page once at build time into .html and .rsc files. On Node.js and Cloudflare Workers, middleware registered with .use() still runs before every static page request, so redirects, auth checks, and response headers work on static pages too:export const app = new Spiceflow() .use(({ request, redirect }) => { if (new URL(request.url).pathname === '/about-us') throw redirect('/about') }) .staticPage('/about', async () => <About />)
ASSETS binding in wrangler.jsonc; the Worker runs your .use() blocks, then returns the stored file without rendering again. See Cloudflare prerendered pages.vite build prerenders the page. That request has no cookies or user headers, so a middleware that redirects it makes the build fail.| When you work on | Read |
Routes, Zod validation, typed errors, middleware, static files, CORS, streaming/SSE, .onError(), noStackTraces, listen(), waitUntil, Node.js adapters, Next.js mounting, base path | API Framework |
| Calling the API with the typed fetch client, WebMCP browser tools | Fetch Client |
| Generating OpenAPI documents, response maps, hiding routes | OpenAPI |
| Exposing routes as LLM tools over Model Context Protocol | MCP |
| OpenTelemetry spans, Server-Timing, custom tracers | Tracing and Strada |
Sending non-JSON types (Date, Map, Set, BigInt) over the wire | Custom Serialization |
| When you work on | Read |
Vite setup, Tailwind, shadcn/ui, app entry, layouts, <Head> SEO, query params, client components, code splitting, router, Link, redirects, 404 pages | React Framework |
Loaders, useLoaderData, streaming with use(), forms, server actions, parseFormData, useActionState, ErrorBoundary | Data & Actions |
The SpiceflowRegister type registry, knownPaths, multi-app workspaces | Type-Safe Routing |
| Rendering remote components from another server | Federation |
| When you work on | Read |
| Writing vitest tests for routes, pages, and server actions | Testing |
| Auth middleware, proxying, non-blocking auth, cookie patterns, graceful shutdown | Middleware Patterns |
| Authenticating server actions and routes — they are public endpoints | Security |
| Porting an app from Remix or React Router | Migrate from Remix |
| How Spiceflow compares to Next.js, Hono, and Elysia | Comparisons |
| Installing and structuring shadcn/ui components | shadcn/ui |
A dependency crashing with useState is undefined at startup | use client trap |
| When you deploy to | Read |
Cloudflare Workers: setup, bindings, waitUntil, KV caching, edge cache | Cloudflare |
| Any host, to understand what happens across deploys | Deployment Skew |
| Cloudflare service bindings between Workers | Service Bindings |
| Docker or any container platform | Docker |
import { createSpiceflowFetch } from 'spiceflow/client' import { SpiceflowTestResponse } from 'spiceflow/testing' import { app } from './main.js' const f = createSpiceflowFetch(app) // API routes return typed JSON const data = await f('/api/hello') expect(data).toEqual({ message: 'Hello, World!' }) // Page routes return SpiceflowTestResponse with rendered HTML const res = await f('/about') if (!(res instanceof SpiceflowTestResponse)) throw new Error('expected page') expect(await res.text()).toContain('About')