Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/brave-bubbles-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"mousse": minor
---

Documentation generation redesign and robustness fixes.

- `document()` becomes `generateDoc(docgen)`, with new `generateHTMLDoc(options)` and `generateOpenAPIDoc(options)` helpers on every Router
- `SchemaParser` classes replaced by the `DocSchemaTranslator` function type, registered per Standard Schema vendor with `addDocSchemaTranslator(vendor, translate)`; registries merge by union when mounting routers
- Doc generation is now strict by default : a schema vendor without translator throws (opt out with `lenient: true`)
- `DefaultBodyParser` now matches content types carrying parameters (`application/json; charset=utf-8`)
- Routes mounted at a router root (`GET /users` for a `/` route mounted on `/users`) now match correctly
- `c.param()` returns `string` instead of `string | undefined` when the param is declared through schemas or generics
- Middlewares and HTTP error handlers can return the context (`return c.status(401).respond()`)
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm

- run: pnpm install --frozen-lockfile

- run: pnpm turbo run build check-types test
36 changes: 36 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Release

on:
push:
branches: [main]

concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm

- run: pnpm install --frozen-lockfile

# Opens/updates a "Version Packages" PR aggregating pending changesets.
# When that PR is merged, builds and publishes to npm.
# version defaults to 'changeset version' : bumps packages and changelogs
- uses: changesets/action@v1
with:
publish: pnpm release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
161 changes: 148 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ Mousse is a Node.js web server framework developed in TypeScript and based on [
- 📄 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

## Installation

> **WARNING**: Mousse is a work in progress, no package is published yet. For now, use it from the monorepo workspace:

```json
{
"dependencies": {
"mousse": "workspace:*"
}
}
```

## Quick Start

```ts
Expand Down Expand Up @@ -49,33 +61,156 @@ new Mousse()

`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.

## TLS

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:

```ts
const app = new Mousse({
key_file_name: 'misc/key.pem',
cert_file_name: 'misc/cert.pem',
passphrase: '1234'
});
```

## WebSockets and Server-Sent Events

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.

```ts
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](docs/websockets.md) and [Server-Sent Events](docs/server-sent-events.md).

## Validation and typing with schemas

Attach [Standard Schema](https://standardschema.dev) schemas (Zod, ArkType, Valibot...) to a route: requests are validated at runtime and the handler is fully typed from them — no generics to write.

```ts
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](docs/schemas.md) for what is validated and when, failure handling and inference details. The same schemas also feed the [documentation generators](docs/docgen.md).

## Documentation

- [Getting started](docs/getting-started.md) — installation, hello world, TLS
- [Routing](docs/routing.md) — routes, patterns, middlewares, routers, error handling
- [Context](docs/context.md) — request and response API
- [Schemas](docs/schemas.md) — validation and type inference
- [Documentation generation](docs/docgen.md) — HTML docs and OpenAPI output
- [Testing routes](docs/testing.md) — firing requests against a router with no real port
- [WebSockets](docs/websockets.md) — upgrades, events, backpressure
- [Server-Sent Events](docs/server-sent-events.md) — sustained requests
- [Modules](docs/modules.md) — body parsers, serializers, loggers, error handlers
- [Working with types](docs/typescript.md) — context types and extensions
1. [Routing](docs/routing.md) — routes, patterns, middlewares, routers, error handling
2. [Context](docs/context.md) — request and response API
3. [Server-Sent Events](docs/server-sent-events.md) — sustained requests
4. [WebSockets](docs/websockets.md) — upgrades, events, backpressure
5. [Modules](docs/modules.md) — body parsers, serializers, loggers, error handlers
6. [Typescript](docs/typescript.md) — context types and extensions
7. [Schemas](docs/schemas.md) — validation and type inference
8. [Testing routes](docs/testing.md) — firing requests against a router with no real port
9. [Documentation generation](docs/docgen.md) — HTML docs and OpenAPI output

## Monorepo

| Path | Content |
|---|---|
| [`packages/mousse`](packages/mousse) | The framework |
| [`apps/examples`](apps/examples) | Runnable examples |
| [`packages/mousse`](packages/mousse) | The framework, with its unit tests |
| [`apps/examples`](apps/examples) | Runnable examples and the framework integration tests |
| [`apps/benchmarks`](apps/benchmarks) | Benchmarks against express, fastify and raw µWebSockets.js |

```sh
pnpm example # list runnable examples
pnpm example schemas # run one on http://localhost:8080
```

## Tests

```sh
pnpm test # turbo run test : builds mousse first, then every suite
```

Two suites, run together by turbo :

- **Unit** — [`packages/mousse/tests`](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`](apps/examples/tests) : real requests fired through `router.test()` covering routing, context, schemas, docgen, SSE — plus a real `listen(0)` + native `WebSocket` client for upgrades, and a smoke test of every example app ([executable documentation](docs/testing.md#dogfooding)).

Each one also runs alone with `pnpm --filter mousse test` or `pnpm --filter @mousse/examples test`. Tests are plain `node --test` : no test framework dependency.

## Benchmarks

`pnpm bench` spawns each server and fires [autocannon](https://github.com/mcollina/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.

## Contributing

> 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.

I haven't yet decided a clear procedure to contribute to this package. I guess the main contribution you can make for now is to test it and open issues to help me chase the bugs. I will try to fix them as fast as I can while thinking of better implementations for performance gains and new handy features. If this package begins to get big attention, I might invite the most interested people to participate in its design and development.
The simplest contribution is to test Mousse and [open issues](https://github.com/Tyrenn/mousse/issues) to help chase the bugs. For code contributions :

1. **Fork** the repository and create a branch from `main` :

```sh
git clone https://github.com/<you>/mousse
cd mousse && pnpm install
git checkout -b my-change
```

2. **Make your change**, with tests. New feature → integration test in [`apps/examples/tests`](apps/examples/tests) (or a unit test in [`packages/mousse/tests`](packages/mousse/tests) for pure logic). Check everything locally :

```sh
pnpm build && pnpm check-types && pnpm test
```

3. **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 :

```sh
pnpm changeset
```

Answer the prompts, then commit the generated `.changeset/*.md` file along with your change.

4. **Open a Pull Request** against `main`. CI runs build, types and the full test suites.

### Releasing (maintainer)

Merged changesets accumulate on `main`; the [release workflow](.github/workflows/release.yml) maintains a "Version Packages" PR aggregating them. Merging that PR bumps versions, updates changelogs and publishes `mousse` to npm (requires the `NPM_TOKEN` repository secret). Manual alternative : `pnpm changeset version && pnpm release`.

## License

[MIT](LICENSE)




> Revoir la fonctionnalité testing (pas vraiment un module ?)
> Mecanique de room pour les sse et ws ?
4 changes: 4 additions & 0 deletions apps/benchmarks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@
"autocannon": "^8.0.0",
"express": "^5.1.0",
"fastify": "^5.4.0",
"hono": "^4.6.0",
"@hono/node-server": "^1.13.0",
"koa": "^2.15.0",
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.67.0"
},
"devDependencies": {
"@core/tsconfig" : "workspace:*",

"@types/express": "^5.0.3",
"@types/koa": "^2.15.0",
"@types/autocannon": "^7.12.7",
"@types/node": "^26.1.1",
"typescript": "5.9.2"
Expand Down
10 changes: 10 additions & 0 deletions apps/benchmarks/src/hono.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Hono } from 'hono'
import { serve } from '@hono/node-server'

const app = new Hono()

app.get('/hello', (c) => c.text('Hello from Hono'))

serve({ fetch: app.fetch, port: 3015 }, () => {
console.log('Hono listening on http://localhost:3015')
})
5 changes: 4 additions & 1 deletion apps/benchmarks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ const servers = [
{ name: 'uWebSockets.js', host: "localhost", port: 3010, file: './dist/uwebsocket.js' },
{ name: 'mousse', host: "localhost", port: 3011, file: './dist/mousse.js' },
{ name: 'express', host: "localhost", port: 3012, file: './dist/express.js' },
{ name: 'fastify', host: "127.0.0.1", port: 3013, file: './dist/fastify.js' }
{ name: 'fastify', host: "127.0.0.1", port: 3013, file: './dist/fastify.js' },
{ name: 'hono', host: "127.0.0.1", port: 3015, file: './dist/hono.js' },
{ name: 'node:http', host: "localhost", port: 3016, file: './dist/nodehttp.js' },
{ name: 'koa', host: "localhost", port: 3017, file: './dist/koa.js' }
]

const waitForPort = (host : string, port : number, timeoutMs = 5000) : Promise<void> => new Promise((resolve, reject) => {
Expand Down
15 changes: 15 additions & 0 deletions apps/benchmarks/src/koa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Koa from 'koa'

const app = new Koa()

// Koa is middleware-only : minimal routing by hand, no @koa/router overhead
app.use((ctx) => {
if (ctx.path === '/hello' && ctx.method === 'GET')
ctx.body = 'Hello from Koa'
else
ctx.status = 404
})

app.listen(3017, () => {
console.log('Koa listening on http://localhost:3017')
})
15 changes: 15 additions & 0 deletions apps/benchmarks/src/nodehttp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createServer } from 'node:http'

// Raw node:http baseline : no routing, no framework
createServer((req, res) => {
if (req.url === '/hello' && req.method === 'GET') {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('Hello from node:http')
}
else {
res.writeHead(404)
res.end()
}
}).listen(3016, () => {
console.log('node:http listening on http://localhost:3016')
})
32 changes: 0 additions & 32 deletions apps/examples/broadcast.ts

This file was deleted.

Loading
Loading