Testing routes
Mousse can fire real HTTP requests against a router or app without binding a real port: RouteTester spins up a single ephemeral instance (listen(0), OS-assigned port) and lets you send requests through it, then closes it.
import { Router } from '@moussejs/core';
const users = new Router()
.get('/users/:id', (c) => ({ id: c.param('id') }))
.post('/users', async (c) => ({ created: await c.body() }));
const client = users.test();
const res = await client.get('/users/42');
await res.json(); // { id: '42' }
const created = await client.post('/users', { name: 'Guillaume' });
await created.json(); // { created: { name: 'Guillaume' } }
await client.close();client.get/post/put/patch/del/head/options return the native fetch Response — no custom assertion API to learn, use it with whatever test runner you like (node:test, vitest, jest...).
One instance per test, not per route
Testing a whole router means firing several requests through the same RouteTester — calling router.test() once and reusing the returned client is what keeps it to a single ephemeral instance, no matter how many routes you exercise:
const client = router.test();
await client.get('/users');
await client.get('/users/1');
await client.post('/users', {...});
await client.del('/users/1');
await client.close(); // one instance, closed onceCalling router.test() again creates a new independent ephemeral instance on a different port — useful for isolating unrelated test files, but avoid calling it more than once per test suite/file if you're testing many routes of the same router.
testRouter: auto-closing helper
testRouter(router, callback) creates the client, runs callback, and closes the instance afterwards — even if the callback throws. Convenient inside a single test case:
import { testRouter } from '@moussejs/core';
test('users routes', () => testRouter(users, async (client) => {
const res = await client.get('/users/42');
assert.equal(res.status, 200);
}));Request bodies
Passing a plain object as the body auto-serializes it as JSON and sets content-type:
await client.post('/users', { name: 'Guillaume' });Pass a string, Buffer, FormData or URLSearchParams to send it as-is (no JSON encoding):
await client.post('/upload', new FormData());The last argument of any verb method is a standard RequestInit for full control (custom headers, signal...):
await client.get('/users/42', { headers: { authorization: 'Bearer token' } });Testing a Mousse app directly
test() and testRouter() work the same way on a full Mousse instance — its routes, middlewares and default handler are all mounted into the ephemeral instance, the real app is never touched (never listens on a real port during the test):
const client = app.test();What isn't covered
RouteTester drives plain HTTP requests — this already covers Server-Sent Events naturally, since a get() response is a regular streamable Response. WebSocket upgrades are not covered yet: testing a ws() route today still requires a real listening instance and a WebSocket client, as in WebSockets.
Dogfooding
Mousse's own test suites are built on RouteTester: the integration tests in apps/examples/tests exercise routing, schemas, docgen and SSE through it, and every example in apps/examples/src exports its app so examples.test.ts smoke-tests it the same way — executable documentation. Everything runs with pnpm test at the repo root.