A lightweight, TypeScript-first library built on unjs/h3 for quickly creating HTTP servers.
- Start a real HTTP server with minimal configuration
- Define nested, method-specific routes declaratively
- Configure middleware declaratively
- Control the server lifecycle and use an available random port by default
- Use native H3 handlers and access the underlying H3 app when needed
npm install kaivoKaivo is ESM-only and requires Node.js 20.16 or newer.
import { createServer } from 'kaivo'
const server = createServer({
routes: {
'/hello': () => ({ message: 'Hello!' })
}
})
await server.listen()
console.log(`Server running at ${server.url}`)The server uses port 0 by default, allowing the operating system to assign an
available port. The resolved address is exposed through server.url and
server.port. Call server.close() when an embedding application or test no
longer needs the server.
A direct route handler responds to GET requests:
const server = createServer({
routes: {
'/ping': () => 'pong'
}
})Use method keys for other HTTP methods, ALL to match every method, and
children to group nested routes:
import { createServer } from 'kaivo'
const server = createServer({
routes: {
'/api': {
children: {
'/users': {
GET: () => [{ id: 1, name: 'Alice' }],
POST: async (event) => {
const body = await event.req.json()
return {
id: 2,
body
}
},
children: {
'/:id': {
GET: (event) => ({
id: event.context.params?.id
})
}
}
}
}
},
'/all': {
ALL: (event) => ({
method: event.req.method
})
}
}
})Use defineRoutes() for type inference when routes are declared separately.
H3 route options are placed alongside handler:
import { defineRoutes } from 'kaivo'
const routes = defineRoutes({
'/users': {
POST: {
handler: createUser,
meta: { name: 'create-user' },
middleware: [requireAuth]
}
}
})Inline routes passed to createServer() or createApp() are already typed.
See the H3 routing guide for matching
behavior.
Pass middleware functions directly, or add a route and H3 middleware options:
import { createServer, defineMiddleware, defineMiddlewares } from 'kaivo'
const requestLogger = defineMiddleware(async (event, next) => {
console.log(event.req.method, event.url.pathname)
return next()
})
const middlewares = defineMiddlewares([
requestLogger,
{
route: '/api/**',
handler: (event, next) => next(),
options: {
method: 'POST'
}
}
])
const server = createServer({ middlewares })Middleware runs in registration order. Use route middleware when behavior belongs to one route by placing it alongside the route handler:
const routes = {
'/secret': {
GET: {
handler: secretHandler,
middleware: [requireAuth]
}
}
}Kaivo re-exports H3's defineMiddleware(). See the
H3 middleware guide for execution
semantics and lifecycle utilities.
Kaivo route handlers are native H3 handlers. You can return JavaScript values
or Web Response objects and use H3 utilities directly:
const routes = {
'/users': {
POST: async (event) =>
Response.json(await event.req.json(), {
status: 201
})
}
}Request parsing, response conversion, errors, cookies, CORS, redirects, streams, proxying, SSE, and WebSocket support belong to H3 and the Web platform. Refer to the H3 documentation for these capabilities:
The H3 app is exposed as server.app. Declarative configuration and native H3
APIs can be used together before listening:
import { createApp, createServer } from 'kaivo'
const app = createApp({
routes: {
'/hello': () => 'Hello!'
}
})
app.get('/health', () => 'ok')
const server = createServer(app, { port: 3000 })
await server.listen()AppOptions extends H3's H3Config, so native H3 configuration can be passed
to createApp(). H3 app plugins belong in the first argument to
createServer(); srvx server plugins belong in the second argument. Kaivo
re-exports H3's definePlugin() for convenience. See the
H3 plugin guide for plugin behavior.
Start the server in suite setup, use its resolved URL for real HTTP requests, and close it during teardown. The default random port avoids conflicts between test workers:
import { createServer } from 'kaivo'
import { afterAll, beforeAll, expect, it } from 'vitest'
const server = createServer({
routes: {
'/users': () => [{ id: 1, name: 'Alice' }]
}
})
beforeAll(async () => {
await server.listen()
})
afterAll(() => server.close())
it('serves users over HTTP', async () => {
const response = await fetch(new URL('/users', server.url!))
expect(response.status).toBe(200)
expect(await response.json()).toEqual([{ id: 1, name: 'Alice' }])
})Jest uses the same beforeAll() and afterAll() pattern. With node:test,
use its before() and after() hooks. See the setup documentation for
Vitest,
Jest, or
node:test.
Kaivo can simulate backend APIs over real HTTP. With Vite, mount the H3 app directly into the development server's middleware stack so the frontend and mock APIs share the same origin without another port or proxy.
Keep the routes and Vite integration in separate files:
mock/
├── routes.ts
└── vite.ts
vite.config.ts
// mock/routes.ts
import { defineRoutes } from 'kaivo'
export const routes = defineRoutes({
'/users': () => [{ id: 1, name: 'Alice' }]
})Create a small Vite plugin that converts the H3 app into Node middleware:
// mock/vite.ts
import type { Plugin } from 'vite'
import { toNodeHandler } from 'h3/node'
import { createApp } from 'kaivo'
import { routes } from './routes'
export function kaivoMock(): Plugin {
return {
name: 'kaivo-mock',
apply: 'serve',
configureServer(viteServer) {
const app = createApp({ routes })
viteServer.middlewares.use('/api', toNodeHandler(app))
}
}
}The Vite configuration only needs to enable the plugin:
// vite.config.ts
import { defineConfig } from 'vite'
import { kaivoMock } from './mock/vite'
export default defineConfig({
plugins: [kaivoMock()]
})The frontend can now request /api/users from the Vite origin. Connect removes
the /api mount prefix before invoking H3, so the corresponding Kaivo route is
/users.
With Vite's default config loader, mock/vite.ts and its statically imported
mock/routes.ts are config dependencies. Changing either file restarts the
development server and creates a new H3 app. The native config loader does
not detect imported config dependencies; see
Vite config loading.
toNodeHandler() comes from the
H3 Node adapter. With webpack-dev-server and other
tools, the same routes can instead be used with a standalone Kaivo server and
an HTTP proxy. See the
webpack-dev-server proxy options.
Use a fixed port when Kaivo is consumed by Postman, mobile or desktop applications, SDK tests, or CI jobs. End-to-end tools such as Playwright and Cypress can start the Kaivo entry file as a dependent process and stop it after the test run.
Creating a controller is synchronous and does not start listening. Pass srvx options as the second argument when a fixed port or other runtime configuration is needed:
const server = createServer(appOrOptions, {
hostname: '127.0.0.1',
port: 3000
})- Register routes, middleware, and plugins before calling
listen(). listen()resolves with the same controller after the raw server is ready.raw,port, andurlare available only while listening.- Calling
listen()while running throws; callclose()before listening again. close()clears the runtime state. Create a new app and controller when app configuration changes.
Creates a server controller from an existing H3 app or declarative
AppOptions. The second argument accepts srvx options except fetch and
manual.
The controller exposes app, raw, port, url, listen(port?), and
close().
Creates an H3 app from native H3 configuration plus declarative routes and
middlewares.
defineRoutes(routes): Type helper for standalone route definitionsdefineMiddlewares(middlewares): Type helper for standalone middleware definitionsdefineMiddleware: Re-export from H3definePlugin: Re-export from H3
See playground/server.ts for a larger working example.