Composing multiple apps on one port
Run two independent apps — each with their own pages directory — on a single port, with no proxy.
The problem
Each call to createServer() binds to a port. If you have a website and a separately installed app (say, a chat service), running both means two ports — and usually a reverse proxy in front.
createRoutes() solves this by separating route discovery from server creation. You build the second app's routes as a plain object, then pass them into your own createServer() call.
How it works
Without composition, the route table for a server is built internally from a single pagesDir. With createRoutes(), you build that table yourself and pass it in via the routes option — which is already supported by createServer() for explicit overrides.
The prefix option prepends a URL path to every discovered route, so the second app's pages live at /chat/login, /chat/channels, etc. and never collide with the host's routes.
Basic example
// server.js
import { createServer, createRoutes } from '@devchitchat/index97'
// Discover and prefix all routes from a second app's pages directory
const chatRoutes = await createRoutes({
pagesDir: './node_modules/@devchitchat/chat/pages',
prefix: '/chat',
csp: "default-src 'self'; connect-src 'self' ws: wss:",
})
// Single server — your site at / and the chat app at /chat
const server = await createServer({
pagesDir: './pages',
port: 3000,
routes: chatRoutes,
websocket: chatWebsocket,
})
Serving a second app's static files
createServer()'s built-in static file handler only knows about its own pagesDir/public/. Static files from the second app's public/ directory need to be registered as explicit routes:
const chatPublicDir = './node_modules/@devchitchat/chat/pages/public'
const glob = new Bun.Glob('**/*')
for await (const file of glob.scan({ cwd: chatPublicDir, onlyFiles: true })) {
chatRoutes['/chat/' + file] = () => new Response(Bun.file(chatPublicDir + '/' + file))
}
Pass those alongside the page routes in the routes option.
Typical pattern: setup() + onServerReady()
Well-designed composable apps export a setup() function that handles their own service wiring, returns pre-built routes, and accepts a callback to run once the server exists (e.g. to attach WebSocket publishing). This keeps all the plumbing inside the app package.
// server.js
import { createServer } from '@devchitchat/index97'
import { setup as setupChat } from '@devchitchat/chat'
const chat = await setupChat({
basePath: '/chat',
dbPath: './data/chat.db',
})
const server = await createServer({
pagesDir: new URL('./pages', import.meta.url).pathname,
port: 3000,
routes: chat.routes,
websocket: chat.websocket,
onShutdown: (server) => { server.stop(); process.exit(0) },
})
chat.onServerReady(server)
routes is the merged routes object (page routes + explicit protocol routes + static file routes, all prefixed). websocket is the Bun WebSocket handler. onServerReady(server) finalizes anything that needs a reference to the live server — like attaching a WebSocket publisher.
createRoutes() options
| Option | Type | Default | Description |
|---|---|---|---|
| pagesDir | string | — | Directory to discover routes from |
| prefix | string | "" | URL prefix prepended to every route pattern (e.g. /chat) |
| dev | boolean | false | Inject HMR script into HTML responses |
| csp | string | default CSP | Content-Security-Policy header value for this app's routes |
| permissionsPolicy | string | camera=(), microphone=(), geolocation=() | Permissions-Policy header value for this app's routes |
| notFoundPage | string | null | Path to a custom 404 page |
Each app's routes carry their own security headers — the CSP and permissions policy you pass to createRoutes() apply only to that app's routes, independent of the host server's headers.
What's not included in the returned routes
- Static files (
pagesDir/public/) — served bycreateServer()'s fetch handler, which only knows its own public dir. Register them as explicit routes if needed (see above). - WebSocket upgrade — add an explicit route for the WS endpoint alongside the returned routes.
- Protocol-specific routes (
/sw.js,/manifest.json, etc.) — add these as explicit routes the same way.