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
22 changes: 11 additions & 11 deletions BENCHMARK.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@

![k6 logo](https://upload.wikimedia.org/wikipedia/commons/e/ef/K6-logo.svg)

Last update: Sat Mar 7 21:08:55 WIB 2026
Last update: Mon Mar 9 00:54:21 WIB 2026

This benchmark measures the performance of Fastro against native Deno `Deno.serve` across various scenarios.

| Scenario | Framework | Throughput (req/s) | Avg Latency | P95 Latency | % of Native | Source |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| **Root** | Native | 67294.34 | 1.4ms | 2.45ms | 100% | [native.ts](native.ts) |
| | Fastro | 64549.85 | 1.46ms | 2.54ms | 95.92% | [main.ts](main.ts) |
| **URL Params** | Native | 58381.92 | 1.62ms | 2.78ms | 100% | [native.ts](native.ts) |
| | Fastro | 61323.13 | 1.54ms | 2.62ms | 105.04% | [main.ts](main.ts) |
| **Query Params** | Native | 50334.73 | 1.9ms | 2.5ms | 100% | [native.ts](native.ts) |
| | Fastro | 54541.10 | 1.74ms | 2.51ms | 108.36% | [main.ts](main.ts) |
| **Middleware** | Native | 60465.19 | 1.56ms | 2.63ms | 100% | [native.ts](native.ts) |
| | Fastro | 61987.21 | 1.52ms | 2.62ms | 102.52% | [main.ts](main.ts) |
| **JSON POST** | Native | 39613.52 | 2.4ms | 3.66ms | 100% | [native.ts](native.ts) |
| | Fastro | 40514.39 | 2.34ms | 3.69ms | 102.27% | [main.ts](main.ts) |
| **Root** | Native | 70061.06 | 1.35ms | 2.47ms | 100% | [native.ts](native.ts) |
| | Fastro | 67104.79 | 1.41ms | 2.51ms | 95.78% | [main.ts](main.ts) |
| **URL Params** | Native | 68501.75 | 1.38ms | 2.35ms | 100% | [native.ts](native.ts) |
| | Fastro | 63042.71 | 1.5ms | 2.63ms | 92.03% | [main.ts](main.ts) |
| **Query Params** | Native | 66118.61 | 1.43ms | 2.32ms | 100% | [native.ts](native.ts) |
| | Fastro | 62095.20 | 1.52ms | 2.64ms | 93.91% | [main.ts](main.ts) |
| **Middleware** | Native | 63877.86 | 1.48ms | 2.67ms | 100% | [native.ts](native.ts) |
| | Fastro | 63489.73 | 1.49ms | 2.57ms | 99.39% | [main.ts](main.ts) |
| **JSON POST** | Native | 42748.01 | 2.22ms | 3.44ms | 100% | [native.ts](native.ts) |
| | Fastro | 40181.28 | 2.37ms | 3.73ms | 94.00% | [main.ts](main.ts) |

## Prerequisites
To run this benchmark locally, ensure you have:
Expand Down
6 changes: 3 additions & 3 deletions core/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,7 @@ Deno.test("autoRegisterModulesFrom logs middleware and route counts when introsp
String((c as { args: unknown[] }).args[0]).includes("Global middlewares")
);
const calledRoutes = info.calls.some((c: unknown) =>
String((c as { args: unknown[] }).args[0]).includes("Registered routes")
String((c as { args: unknown[] }).args[0]).includes("Registered route(s)")
);
assert(
calledMw && calledRoutes,
Expand Down Expand Up @@ -1445,7 +1445,7 @@ Deno.test("autoRegisterModulesFrom logs middleware and route counts when introsp
String((c as { args: unknown[] }).args[0]).includes("Global middlewares")
);
const calledRoutes = info.calls.some((c: unknown) =>
String((c as { args: unknown[] }).args[0]).includes("Registered routes")
String((c as { args: unknown[] }).args[0]).includes("Registered route(s)")
);
assert(
calledMw && calledRoutes,
Expand Down Expand Up @@ -1606,7 +1606,7 @@ Deno.test("autoRegisterModulesFrom logs middleware and route counts when availab
);
const foundRoutes = infoStub.calls.some((c: unknown) =>
String((c as { args: unknown[] }).args[0]).includes(
"Registered routes: 2",
"Registered route(s): 2",
)
);
assert(foundMw, "expected middleware count log");
Expand Down
10 changes: 5 additions & 5 deletions core/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,12 @@ export function autoRegisterModulesFrom(
const regs = _getRegisteredMounts();
if (regs.length) {
console.info(
`ℹ️ [Loader] Registered ${regs.length} module(s): ${
regs.map((r) => `${r.name}@${r.mount}`).join(", ")
`ℹ️ [Loader] Registered ${regs.length} module(s): ${
regs.map((r) => `${r.name} @ ${r.mount}`).join(", ")
}`,
);
} else {
console.info("ℹ️ [Loader] No modules registered by loader");
console.info("ℹ️ [Loader] No modules registered by loader");
}

// If the app exposes middleware/route introspection, include counts.
Expand All @@ -159,10 +159,10 @@ export function autoRegisterModulesFrom(
: undefined;

if (typeof mwCount === "number") {
console.info(`ℹ️ [Loader] Global middlewares: ${mwCount}`);
console.info(`ℹ️ [Loader] Global middlewares: ${mwCount}`);
}
if (Array.isArray(routePaths)) {
console.info(`ℹ️ [Loader] Registered routes: ${routePaths.length}`);
console.info(`ℹ️ [Loader] Registered route(s): ${routePaths.length}`);
}
} catch {
// Non-fatal: logging should not break app startup
Expand Down
3 changes: 3 additions & 0 deletions core/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Deno.test("createRouter forwards GET to server", () => {
head: () => undefined,
options: () => undefined,
use: () => undefined,
hook: () => undefined,
serve: () => ({ close: () => undefined }),
} as Parameters<typeof createRouter>[0];

Expand All @@ -39,6 +40,7 @@ Deno.test("builder.build returns noop middleware", async () => {
head: () => undefined,
options: () => undefined,
use: () => undefined,
hook: () => undefined,
serve: () => ({ close: () => undefined }),
} as Parameters<typeof createRouter>[0];
const r = createRouter(mock);
Expand All @@ -62,6 +64,7 @@ Deno.test("createRouter forwards all methods to server", () => {
head: () => called.push("head"),
options: () => called.push("options"),
use: () => undefined,
hook: () => undefined,
serve: () => ({ close: () => undefined }),
} as Parameters<typeof createRouter>[0];

Expand Down
105 changes: 105 additions & 0 deletions core/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2726,3 +2726,108 @@ Deno.test("Coverage - applyMiddlewares len===2 via tryRoute (first hit)", async
assertEquals(await res.text(), "two-mw-ok");
s.close();
});

// ──────────────────────────────────────────────────────────────────────────
// Hook coverage: onRequest only, onResponse async (Promise + sync result)
// ──────────────────────────────────────────────────────────────────────────

Deno.test("Coverage - hook onResponse only (no onRequest)", async () => {
_resetForTests();
server.hook("onResponse", (_req: Request, _ctx: Context, next: Next) => {
return next();
});
server.get("/hook-resp-only", () => new Response("resp-only"));
const s = server.serve({ port: 3643 });
const res = await fetch("http://localhost:3643/hook-resp-only");
assertEquals(await res.text(), "resp-only");
s.close();
});

Deno.test("Coverage - hook onRequest only (no onResponse)", async () => {
_resetForTests();
let ran = false;
server.hook("onRequest", (_req: Request, _ctx: Context, next: Next) => {
ran = true;
return next();
});
server.get("/hook-req-only", () => new Response("ok"));
const s = server.serve({ port: 3640 });
const res = await fetch("http://localhost:3640/hook-req-only");
assertEquals(await res.text(), "ok");
assert(ran);
s.close();
});

Deno.test("Coverage - hook onResponse async with Promise result", async () => {
_resetForTests();
server.hook("onRequest", (_req: Request, ctx: Context, next: Next) => {
ctx.state = { tag: "req" };
return next();
});
server.hook(
"onResponse",
async (_req: Request, _ctx: Context, next: Next) => {
// async hook — result of next() is a Promise when route handler is async
const res = await next() as Response;
return new Response((await res.text()) + "+async", {
status: res.status,
});
},
);
// async route so handleRequest returns a Promise → covers "result instanceof Promise" branch
server.get("/hook-async-promise", async () => {
await Promise.resolve();
return new Response("body");
});
const s = server.serve({ port: 3641 });
const res = await fetch("http://localhost:3641/hook-async-promise");
assertEquals(await res.text(), "body+async");
s.close();
});

Deno.test("Coverage - hook onResponse async with sync result", async () => {
_resetForTests();
server.hook("onRequest", (_req: Request, ctx: Context, next: Next) => {
ctx.state = { tag: "req" };
return next();
});
server.hook(
"onResponse",
async (_req: Request, _ctx: Context, next: Next) => {
const res = await next() as Response;
return new Response((await res.text()) + "+async-sync", {
status: res.status,
});
},
);
// sync route so handleRequest returns a Response (not a Promise)
server.get("/hook-async-sync", () => new Response("body"));
const s = server.serve({ port: 3642 });
const res = await fetch("http://localhost:3642/hook-async-sync");
assertEquals(await res.text(), "body+async-sync");
s.close();
});

Deno.test("Coverage - hook + cache hit uses sharedCtx (no new FastContext)", async () => {
_resetForTests();
server.hook(
"onRequest",
(_req: Request, _ctx: Context, next: Next) => next(),
);
server.hook(
"onResponse",
(_req: Request, _ctx: Context, next: Next) => next(),
);
server.get(
"/hook-cache",
(_req, ctx) => new Response(`id:${ctx.params.id ?? "none"}`),
);
const s = server.serve({ port: 3643 });
// First request: non-cached path
const r0 = await fetch("http://localhost:3643/hook-cache");
await r0.body?.cancel();
// Second request: cache hit path with sharedCtx populated
const res = await fetch("http://localhost:3643/hook-cache");
assertEquals(await res.text(), "id:none");
s.close();
});
Loading