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)
What setup() returns. 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

OptionTypeDefaultDescription
pagesDirstringDirectory to discover routes from
prefixstring""URL prefix prepended to every route pattern (e.g. /chat)
devbooleanfalseInject HMR script into HTML responses
cspstringdefault CSPContent-Security-Policy header value for this app's routes
permissionsPolicystringcamera=(), microphone=(), geolocation=()Permissions-Policy header value for this app's routes
notFoundPagestringnullPath 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 by createServer()'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.