Guides

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: defineRoutes with groups, prefixes, named routes, resource('projects', Controller) for the five REST actions, per-route and per-group middleware (cors(), rateLimit(), requireJson(), your own defineMiddleware).
  • 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 to shutdown with a timeout. The request-scoped container (useRequestApp()) forks the app per request.
  • Realtime: createRealtimeServer({ channels, authorize }) over WebSocket (crossws), Socket.IO or SSE, chosen in config/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/server gives authMiddleware({ verify }), requireUser(), currentUser() and can() on the API side with the same defineAbilities the client uses.
  • Database: defineDb({ connect, disconnect }) in config/db.ts with the ORM of your choice; no ORM is bundled.
  • Typed client for the frontend: export type AppRoutes = typeof routes.$types in 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.