A backend without React
Build an API server with controllers, a typed client and realtime channels, and no React anywhere.
kitr new my-api --kit server creates a backend whose dependency graph contains no React, no TanStack and no Vite:
my-api/
kitrrc.ts # platforms: { api: server({ runtime: 'node' }) }, providers: logger, db, realtime
server/main.ts # createServer({ providers, config }) + routes + realtime + listen()
server/routes.ts # defineRoutes(...)
server/controllers/ # plain classes, one per resource
server/realtime.ts # createRealtimeServer({ channels })
config/ # one file per package; config/index.ts is generated by `kitr add`
app/channels/ # defineChannel(...)
app/env.ts # createEnv({ server: { PORT, LOG_LEVEL, DATABASE_URL } })
test/ # createTestClient(server).get('/api/health')
Run it with kitr dev (tsx watch, or bun --watch, deno run --watch, wrangler dev depending on runtime) or kitr build && kitr start. kitr config api runtime bun switches runtime; kitr config api port 8080 or KITR_API_PORT=8080 changes the port. kitr api:sync writes openapi.json and a route table; kitr routes:list prints the routes.
What you get
- Router:
defineRouteswith groups, prefixes, named routes,resource('projects', Controller)for the five REST actions, per-route and per-group middleware (cors(),rateLimit(),requireJson(), your owndefineMiddleware). - Controllers: classes whose methods take an
HttpContext(params,query,body,request,response,locals,logger,requestId,app).static validation = { store: { body: schema } }validates before the method runs and types the client. - Errors: throw
KitrError.notFound('Project'),KitrError.validation(fieldErrors)or anything else; every response has the same{ code, message, status, fieldErrors? }shape the client already understands. - Lifecycle: providers run
register → boot → ready → shutdown;server.listen()wires SIGINT/SIGTERM toshutdownwith a timeout. The request-scoped container (useRequestApp()) forks the app per request. - Realtime:
createRealtimeServer({ channels, authorize })over WebSocket (crossws), Socket.IO or SSE, chosen inconfig/realtime.ts;realtime.use(channel, params).emit(event, data)from a controller. The client side (kitr/realtime) can live in any frontend, kitr or not. - Auth:
kitr/auth/servergivesauthMiddleware({ verify }),requireUser(),currentUser()andcan()on the API side with the samedefineAbilitiesthe client uses. - Database:
defineDb({ connect, disconnect })inconfig/db.tswith the ORM of your choice; no ORM is bundled. - Typed client for the frontend:
export type AppRoutes = typeof routes.$typesin the backend,createApiClient<AppRoutes>()in the frontend. Share the type through a workspace package or a.d.ts; there is no generated code to keep in sync.
Mixing
A kitr React app that later needs its own backend keeps the same packages: kitr add server api adds the api platform, server/main.ts and app/routes/api.$.ts, which mounts the same router inside TanStack Start, so kitr dev runs both and kitr dev site runs just the UI. A standalone backend plus a --kit app frontend is the other valid shape; kitr doctor keeps React out of the backend either way.