Mousse is a Node.js webserver framework developed in TypeScript and based on µWebSockets.js. It provides simple ways to route HTTP requests and handle WebSockets or Server-Sent Events through a single, consistent Context API. It uses a middlewares-based structure and handlers as inspired by expressJS and fastify.
In french Mousse refers to soap foam, composed of thousands bubbles : the requests 🫧
- ⚡ Built on µWebSockets.js, one of the fastest HTTP/WS servers available for Node.js
- 🌐 HTTP routing with parameters, wildcards, route options and composable routers
- 🔌 WebSockets with automatic upgrade, per-route events and backpressure handling
- 📡 Server-Sent Events as sustained requests, with backpressured send queue
- 🧩 Swappable modules per route or per router : body parser, response serializer, logger, error handlers
- 🛡️ Fully typed : request body, response, route params and context extensions
- ✅ Schema validation through Standard Schema : bring your Zod, ArkType or Valibot schemas as-is, get runtime validation and type inference
- 📄 Documentation generation from route schemas : self-styled HTML site or standard OpenAPI 3.1 output
- 🧪 One-call route testing :
router.test()fires real requests through a single ephemeral instance, no port management
pnpm add @tyren/mousse # or npm install @tyren/mousseimport { Mousse } from '@tyren/mousse';
new Mousse()
.get('/hello', (c) => {
return 'Hello World !';
})
.get('/users/:id', (c) => {
return { id: c.param('id') };
})
.post('/echo', async (c) => {
return await c.body();
})
.ws('/chat', (c) => {
c.onMessage((ws, message) => ws.send(message));
})
.sse('/events', (c) => {
c.send('welcome');
})
.listen(3000, (ls, port) => {
if (ls) console.log(`Listening on port ${port}`);
});c is a Context instance : it wraps the request and the response and carries every utility you need. A handler may respond explicitly (c.respond(), c.json()...) or simply return a value.
Every registration method returns the app itself, so the whole application can be chained — and stopped with app.stop(), which closes the listen socket while letting in-flight work finish.
Pass the µWebSockets.js SSL options to the constructor. When both key_file_name and cert_file_name are provided, Mousse automatically creates an SSL application:
const app = new Mousse({
key_file_name: 'misc/key.pem',
cert_file_name: 'misc/cert.pem',
passphrase: '1234'
});Real-time channels use the same routing and Context API as HTTP.
A ws route separates the upgrade phase — plain HTTP middlewares that can reject the connection by responding — from the connected phase, where the handler receives an already-open WSContext with events, pub/sub and automatic backpressure handling.
app.ws('/chat/:room',
(c) => {
if (!c.getHeader('authorization')) return c.status(401).respond(); // upgrade rejected
},
(c) => {
c.subscribe(c.param('room'));
c.onMessage((ws, message) => c.publish(c.param('room'), message));
});An sse route hands you an already-sustained context: c.send() pushes events through a backpressured queue, and any regular route can open a channel itself with c.sustain().
See WebSockets and Server-Sent Events.
Attach Standard Schema schemas (Zod, ArkType, Valibot...) to a route: requests are validated at runtime and the handler is fully typed from them — no generics to write.
import { z } from 'zod';
app.post('/orgs/:org/users', {
schemas: {
Body: z.object({ name: z.string(), email: z.string().email() }),
Response: z.object({ id: z.number() })
}
}, async (c) => {
const body = await c.body(); // { name: string; email: string } — validated AND typed
return { id: 1 }; // must match the Response schema
});See Schemas for what is validated and when, failure handling and inference details. The same schemas also feed the documentation generators.
- Routing — routes, patterns, middlewares, routers, error handling
- Context — request and response API
- Server-Sent Events — sustained requests
- WebSockets — upgrades, events, backpressure
- Modules — body parsers, serializers, loggers, error handlers
- Typescript — context types and extensions
- Schemas — validation and type inference
- Testing routes — firing requests against a router with no real port
- Documentation generation — HTML docs and OpenAPI output
| Path | Content |
|---|---|
packages/mousse |
The framework, with its unit tests |
apps/examples |
Runnable examples and the framework integration tests |
apps/benchmarks |
Benchmarks against express, fastify and raw µWebSockets.js |
pnpm example # list runnable examples
pnpm example schemas # run one on http://localhost:8080pnpm test # turbo run test : builds mousse first, then every suiteTwo suites, run together by turbo :
- Unit —
packages/mousse/tests: pure logic of the package (uri joining, query parsing, Standard Schema validation, JSON Schema rendering, doc translators). - Integration & examples —
apps/examples/tests: real requests fired throughrouter.test()covering routing, context, schemas, docgen, SSE — plus a reallisten(0)+ nativeWebSocketclient for upgrades, and a smoke test of every example app (executable documentation).
Each one also runs alone with pnpm --filter @tyren/mousse test or pnpm --filter @mousse/examples test. Tests are plain node --test : no test framework dependency.
pnpm bench spawns each server and fires autocannon at a plain-text route — 100 connections, 10 seconds, Node 24, same machine for all:
| Framework | req/s | avg latency | p99 latency |
|---|---|---|---|
| Mousse | 41 085 | 2.16 ms | 5 ms |
| µWebSockets.js (raw) | 39 277 | 2.22 ms | 5 ms |
| node:http (raw) | 36 576 | 2.23 ms | 5 ms |
| koa | 31 352 | 2.68 ms | 5 ms |
| fastify | 30 286 | 2.72 ms | 7 ms |
| hono | 26 095 | 3.35 ms | 8 ms |
| express | 19 884 | 4.50 ms | 9 ms |
Mousse runs at raw µWebSockets.js speed — the two are within measurement noise of each other — while adding routing, middlewares, validation and the whole Context API. Every framework built on node:http starts below the second baseline; Mousse doesn't. Single 10-second runs, numbers vary with hardware and differences under ~5% aren't significant : run pnpm bench to reproduce on yours.
Disclaimer : even though I'm proud of this version, it represents one of my first public, ambitious and TypeScript based modules. Many improvements need to be done and will be, I hope.
The simplest contribution is to test Mousse and open issues to help chase the bugs. For code contributions :
-
Fork the repository and create a branch from
main:git clone https://github.com/<you>/mousse cd mousse && pnpm install git checkout -b my-change
-
Make your change, with tests. New feature → integration test in
apps/examples/tests(or a unit test inpackages/mousse/testsfor pure logic). Check everything locally :pnpm build && pnpm check-types && pnpm test
-
Record your change with a changeset — it describes the change and the version bump it deserves (patch/minor/major), and ends up in the changelog :
pnpm changeset
Answer the prompts, then commit the generated
.changeset/*.mdfile along with your change. -
Open a Pull Request against
main. CI runs build, types and the full test suites.
Merged changesets accumulate on main. Releasing is manual, from an up-to-date main with npm credentials (npm login) :
pnpm changeset version # consume changesets : bump versions, write changelogs
git add -A && git commit -m "Version packages"
pnpm release # build + check-types + test, then changeset publish
git push --follow-tags # push the version commit and the release tags