Each source folder serves a specific concern - marketing site, customer app, admin, etc.
Yet, development workflow is identical.
One dev server covers both sides of a folder. Client modules go through Vite with HMR; API routes run in the same process and hot-reload on change.
There is no second command to start and no proxy to configure - requests are dispatched between the two by path.
Starting the Dev Server
pnpm dev # all source folders
pnpm dev front # specific folder (front, admin, app, etc.)Default port is 4556, configured as devPort in package.json.
What Happens on Start
- Generators run, seeding any blank route or page files - see code generation
Vitecompilesapi/app.ts- Dev server starts, serving both client pages and your API routes
- Requests are routed between Vite and your API by the folder's
apiBase - File watchers monitor client modules and API files for changes
Dev Renders Client-Side
pnpm dev is always Vite + HMR + client-side rendering, even when folder has SSR enabled.
Server rendering happens in the production build, so there is no server-rendered markup to look at while the dev server is running. This catches people out coming from Next/Nuxt/TanStack Start, where dev mirrors production rendering.
To see, test or debug anything server-rendered, run kosmo preview. It serves the real production build and still reloads when you edit a file.
Hot Reload vs HMR
The two sides of a source folder reload differently:
- Client: HMR - changed modules are patched in place, component state survives the edit. A component, a style, a page - the browser updates without a navigation.
- API: hot reload - on change, the API program restarts as a whole. There is no HMR for the backend, and reloads fire on more than route edits (config changes, shared types etc.)
A full restart means module-level state resets on every reload. This is by design, not a limitation to work around: a backend should be stateless, in development for fast reliable reloads and in production so it can restart, scale, and run as multiple instances.
Keep anything that must survive a restart in a real store from day one - the dev reload cycle is simply an early rehearsal of what production restarts do anyway.
What does need care across reloads is resources: open connections leak when the program restarts around them. Close them in the teardownHandler hook.
api/dev.ts
Client-side dev behaviour is Vite's, configured through the folder's kosmo.config.ts like any Vite project. The API side has its own hooks, because it is the part kosmo runs itself.
api/dev.ts exposes three hooks for customizing the dev experience.
requestHandler
Returns the API request handler. Generated default:
import { getRequestListener } from "@hono/node-server";
import { devSetup } from "_/api:factory";
import app from "./app";
export default devSetup({
requestHandler() {
return getRequestListener(app.fetch);
},
});Override this for custom routing logic - WebSocket handling, multi-handler dispatch, etc.
requestMatcher
Controls which requests go to your API vs Vite.
export default devSetup({
requestHandler() {
// ...
},
requestMatcher(req) {
return req.url?.startsWith("/api") ||
req.headers["x-api-request"] === "true";
},
});teardownHandler
Runs before each API reload. Use it to close connections and release resources that would otherwise leak across rebuilds:
let dbConnection;
export default devSetup({
requestHandler() {
// ...
},
async teardownHandler() {
if (dbConnection) {
await dbConnection.close();
dbConnection = undefined;
}
},
});Without cleanup, frequent rebuilds during active development can exhaust database connections.
TIP
This hook is dev-only. In production the process exits and the OS reclaims everything; it exists because a dev reload restarts the API inside a long-running process.
Inspecting API Routes
Routes can be inspected by providing debug option to appFactory in api/app.ts (omitted by default, feel free to add it as needed):
import appFactory, { routes } from "_/api:factory";
import defaultErrorHandler from "./errors";
export default appFactory(
routes,
{ debug: true },
({ app }) => {
// ...
})Example output:
/api [ index/index.ts ]
methods: GET|HEAD
middleware: slot: @extendContext useExtendContext
slot: validate:params useValidateParams
handler: indexHandlerNamed middleware functions show by name; anonymous ones show their first line. Name your middleware functions - it makes this output significantly easier to read.
Individual debug properties are also available for targeted output: headline, methods, middleware, handler.
Use this to display only headline:
export default appFactory(routes, { debug: "headline" }, ({ app }) => {
// ...
})If you rather need a custom logger, provide a function instead; it will be provided with full debug object and the route itself.
export default appFactory(
routes,
{
debug(log, route) {
console.log(log.full);
},
},
({ app }) => {
// ...
},
);The log signature:
{
headline: string;
methods: string;
middleware: string;
handler: string;
full: string;
}