A small, zero-dependency HTTP application core that runs the same code across Bun, Cloudflare Workers, Deno, Node.js, and on browser Service Worker.
npm install @litejs/server
// app.mjs
import { App } from "@litejs/server"
const app = App()
// Middlewares runs only if any route matches
app.use((req, env) => {
// return a response to stop further execution
})
// Only single, first matching route get executed!
app.get('hello/world', (res, env) => 'Hello MOON!')
app.get('hello/{name}', (res, env) => 'Hello ' + req.param.name)
app.get('bye/{name}', (res, env) => 'Bye ' + req.param.name)
app.get('bye/moon', (res, env) => { /* Never executed as previous handler matches */ })
app.get('teapot', () => ({ body: "no coffee", status: 418 }))
app.get('notFound', () => 404) // Return a number to send status code
// Group routes and mount under a prefix
const subApp = App()
.post("", (req, env) => {
// GET /api -> req.path == '/' and req.fullPath == '/api'
return { data: [] }
})
.post("echo", async (req, env) => {
// POST /api/echo -> req.path == '/echo' and req.fullPath == '/api/echo'
return await req.json()
})
app.mount("api", subApp)
export default appHandlers receive (req, env) and may return
a string (sent as text/plain),
a number (status only),
an object (serialized to JSON),
a { body, status, headers } object,
or a native Response.
Thrown errors map to err.code || 500.
Requests include param, path, fullPath, query, searchParams, and header(name).
user/{username}matches one path segment (no/)post/{id+}matches one or more digitsfiles/{rest*}.extmatches all chars, greedya/{dir/}{name}matches zero or more slash-terminated directoriespub/\{x}matches the literal pathpub/{x}
The same app runs on every runtime.
@litejs/server exports the matching adapter through conditional export,
so the one file below runs unchanged on Node.js, Bun, and Deno.
// run.mjs
import {
DB, KV, listen, loadEnv, serveStatic, setupShutdown
} from "@litejs/server"
import app from "./app.mjs"
const db = new DB("db.sqlite")
const env = loadEnv(".env.json", {
ASSETS: serveStatic("public"),
// Inject Cloudflare style KV on top of sqlite
DEVICE: KV(db, "device"),
})
const server = listen(app, env)
app.get("/{path*}", env.ASSETS.fetch)
// Attach SIGINT/SIGTERM/SIGHUP/uncaughtException
setupShutdown([ server ])Run it with node run.mjs, bun run.mjs, or deno run -A run.mjs.
- Node.js, Bun, Deno: import from
@litejs/server; the runtime is detected automatically. - Cloudflare Workers:
import { worker } from "@litejs/server", thenexport default { fetch: worker(app) }. - Service Workers:
import { listen } from "@litejs/server".
On Node, Bun, and Deno @litejs/server also exports serveStatic, loadEnv,
setupShutdown, and SQLite-backed DB (D1) and KV shims.
Runnable examples are in test/server/.
Copyright (c) 2026 Lauri Rooden <lauri@rooden.ee>
MIT License | GitHub repo | npm package | Buy Me A Tea