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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ npm i @chttp/router
Import:

```ts
import { createRouter } from "@chttp/router";
import { CtxCore, createRouter } from "@chttp/router";

const app = createRouter();

app.get("/", (c) => c.text("hi"));
app.get("/user/:id", (c) => c.json({ id: c.req.param("id") }));
app.post("/echo", (c) => c.text(c.req.body()));
app.notFound((c) => c.status(404).text("not found"));
app.get("/", (c: CtxCore) => c.text("hi"));
app.get("/user/:id", (c: CtxCore) => c.json({ id: c.req.param("id") }));
app.post("/echo", (c: CtxCore) => c.text(c.req.body()));
app.notFound((c: CtxCore) => c.status(404).text("not found"));

app.serve({ port: 3000 });
```
Expand All @@ -39,8 +39,8 @@ app.serve({ port: 3000 });
Static segments, `:param`, and a tail `*` wildcard:

```ts
app.get("/user/:id", (c) => c.req.param("id")); // 'id' is literal-typed
app.get("/wild/*", (c) => c.req.param("*"));
app.get("/user/:id", (c: CtxCore) => c.req.param("id")); // 'id' is literal-typed
app.get("/wild/*", (c: CtxCore) => c.req.param("*"));
app.all("/any-method", handler);
app.on("m-search", "/custom", handler);
```
Expand Down Expand Up @@ -88,13 +88,13 @@ c.redirect("/elsewhere", 302);
registered after it (snapshot semantics):

```ts
app.use((c, next) => {
app.use((c: CtxCore, next) => {
c.header("x-start", Date.now().toString());
next();
});

// path-scoped: `*` matches every path, `/api/*` is a prefix
app.use("/api/*", async (c, next) => {
app.use("/api/*", async (c: CtxCore, next) => {
await auth(c.req);
await next();
});
Expand All @@ -116,7 +116,7 @@ at 100% static).

```ts
const app = createRouter();
app.get("/user/:id", (c) => c.json({ id: c.req.param("id") }));
app.get("/user/:id", (c: CtxCore) => c.json({ id: c.req.param("id") }));

// Cloudflare Workers / Deno Deploy / Bun / Service Worker
export default { async fetch(req, env, ctx) { return app.fetch(req, env, ctx); } };
Expand Down Expand Up @@ -201,7 +201,7 @@ same band as [valibot](https://github.com/open-circle/valibot).
import * as v from "@chttp/val";

const Order = v.strictObject({ id: v.pipe(v.string(), v.uuid()) });
app.post("/order", (c) => {
app.post("/order", (c: CtxCore) => {
const r = v.safeParse(Order, JSON.parse(c.req.body()));
return r.success
? c.json({ ok: true })
Expand Down
4 changes: 2 additions & 2 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ Use this when you need npm packages outside the scriptc subset. See [Run targets
The canonical entry is `app.fetch(req: Request) → Promise<Response>`. The same `createRouter()` symbol works on Cloudflare Workers, Deno Deploy, Bun, Vercel Edge, Netlify Edge, Supabase, and as a Service Worker. `app.handler()` is a thin Node adapter around `fetch()` for `node:http` users.

```ts
import { createRouter } from "@chttp/router";
import { CtxCore, createRouter } from "@chttp/router";

const app = createRouter();
app.get("/user/:id", (c) => c.json({ id: c.req.param("id") }));
app.get("/user/:id", (c: CtxCore) => c.json({ id: c.req.param("id") }));

// Cloudflare Workers / Deno Deploy / Bun / Vercel Edge / Service Worker
export default {
Expand Down
3 changes: 2 additions & 1 deletion docs/html.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@
| `isVoidElement`, `isValidTag`, `isValidAttrName` | `(s: string) => boolean` | Exposed helpers |

```ts
import { CtxCore } from "@chttp/router";
import { BARE, escapeHtml, h, html } from "@chttp/html";

function Todo(props: { text: string; done: boolean }): string {
return h("li", ["class", props.done ? "done" : "open"], [escapeHtml(props.text)]);
}

app.get("/todos", (c) =>
app.get("/todos", (c: CtxCore) =>
c.html(
h("ul", [], [Todo({ text: "<b>ship</b>", done: false })]) +
h("input", ["disabled", BARE, "value", "x"], []),
Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
chttp is a minimal async-first HTTP router for `node:http`, authored inside the [scriptc](https://github.com/vercel-labs/scriptc) compilable subset so the same source also compiles to a single native ELF.

```ts
import { createRouter } from "@chttp/router";
import { CtxCore, createRouter } from "@chttp/router";

const app = createRouter();
app.get("/user/:id", async (c) => c.json(await fetchUser(c.req.param("id"))));
app.get("/user/:id", async (c: CtxCore) => c.json(await fetchUser(c.req.param("id"))));
app.serve({ port: 3000 });
```

Expand Down
26 changes: 13 additions & 13 deletions docs/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Middleware is `(c, next) => void | Promise<void>` — sync or async. `next()` returns the tail's result, so `await next()` in an async middleware waits for the rest of the chain to settle.

```ts
app.use((c, next) => {
app.use((c: CtxCore, next) => {
c.header("x-start", Date.now().toString());
next();
});
Expand All @@ -26,16 +26,16 @@ Every snippet below is written in the scriptc subset (indexed `for` loops, `| nu
Two pieces, because of how dispatch orders things: an `OPTIONS` request to a path that only has `GET`/`POST` registered fails with 405 *before any middleware runs*, so a middleware alone cannot answer preflight. The working shape is a `use()` middleware for the response headers plus one explicit `app.options()` route per path (or `app.all()` with an in-handler method check) that answers the preflight with 204.

```ts
import type { Ctx } from "@chttp/router";
import type { CtxCore } from "@chttp/router";

// 1. Response headers on every actual request (apply to routes after this use()).
app.use((c: Ctx, next: () => unknown): unknown => {
app.use((c: CtxCore, next: () => unknown): unknown => {
c.header("access-control-allow-origin", "*");
return next();
});

// 2. Explicit preflight answer: 204, no body.
function preflight(c: Ctx): void {
function preflight(c: CtxCore): void {
c.status(204)
.header("access-control-allow-origin", "*")
.header("access-control-allow-methods", "GET,POST,PUT,DELETE,OPTIONS")
Expand All @@ -48,11 +48,11 @@ function preflight(c: Ctx): void {
c.sent = true; // tells dispatch this middleware answered
}

app.options("/users", (c: Ctx) => {
app.options("/users", (c: CtxCore) => {
preflight(c);
});
app.get("/users", (c: Ctx) => c.json({ users: 1 }));
app.post("/users", (c: Ctx) => c.json({ ok: true }), { body: true });
app.get("/users", (c: CtxCore) => c.json({ users: 1 }));
app.post("/users", (c: CtxCore) => c.json({ ok: true }), { body: true });
```

With credentials instead of `*`: echo the request's `origin` header and add `access-control-allow-credentials: true`. For many paths, a shared `app.all("/*", ...)` fallback also answers preflight (a splat route matches paths with no other registration), but paths that have any method registered need their own `app.options()` entry or the 405 wins.
Expand All @@ -71,7 +71,7 @@ function fnv1a(s: string): string {
return h.toString(16);
}

app.get("/users", (c: Ctx) => {
app.get("/users", (c: CtxCore) => {
const body: string = JSON.stringify({ users: [1, 2] });
const etag: string = '"' + fnv1a(body) + '"';
const inm: string | null = c.req.header("if-none-match");
Expand Down Expand Up @@ -101,7 +101,7 @@ The comparison is exact equality against a single etag. Browsers may send a comm
Async middleware: `await next()` waits until the chain settles, so `Date.now()` after it includes the handler's async work. Duration goes to stdout, not a response header, because the response may already be flushed (the flush rule above).

```ts
app.use(async (c: Ctx, next: () => unknown): Promise<void> => {
app.use(async (c: CtxCore, next: () => unknown): Promise<void> => {
const t0: number = Date.now();
await next();
console.log(c.req.method + " " + c.req.path + " " + String(Date.now() - t0) + "ms");
Expand All @@ -115,7 +115,7 @@ Register it before the routes you want logged; routes registered earlier carry n
Sync middleware that sets headers before `next()`; they land on every response from routes registered after it.

```ts
app.use((c: Ctx, next: () => unknown): unknown => {
app.use((c: CtxCore, next: () => unknown): unknown => {
c.header("x-content-type-options", "nosniff");
c.header("x-frame-options", "DENY");
c.header("referrer-policy", "no-referrer");
Expand All @@ -141,7 +141,7 @@ const WINDOW_MS: number = 60000; // window length

const buckets: { [k: string]: Bucket } = {};

function rateLimit(c: Ctx, next: () => unknown): unknown {
function rateLimit(c: CtxCore, next: () => unknown): unknown {
const key: string = c.req.header("x-api-key") === null ? "anon" : String(c.req.header("x-api-key"));
const now: number = Date.now();
const cur: Bucket | null = buckets[key] === undefined ? null : buckets[key];
Expand Down Expand Up @@ -177,13 +177,13 @@ A true `server-timing: app;dur=N` header needs the duration *before* the body is

```ts
// start stamp: header lands (set before the handler can flush)
app.use((c: Ctx, next: () => unknown): unknown => {
app.use((c: CtxCore, next: () => unknown): unknown => {
c.header("x-request-start", String(Date.now()));
return next();
});

// duration: log it, the response is already flushed by the time you know it
app.use(async (c: Ctx, next: () => unknown): Promise<void> => {
app.use(async (c: CtxCore, next: () => unknown): Promise<void> => {
const t0: number = Date.now();
await next();
console.log(c.req.path + " " + String(Date.now() - t0) + "ms");
Expand Down
4 changes: 2 additions & 2 deletions docs/multipart.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
`multipart/form-data` uploads are opt-in per route — the same seam as `{ body: true }`. Declare `multipart` in the route options and dispatch parses the stream before the handler chain runs, so `c.req` stays synchronous.

```ts
import { createRouter, diskStorage, memoryStorage } from "@chttp/router";
import { CtxCore, createRouter, diskStorage, memoryStorage } from "@chttp/router";

const app = createRouter();

app.post(
"/api/entries/:entryId/media",
(c) => {
(c: CtxCore) => {
const file = c.req.file("file"); // UploadedFile | null
const title = c.req.field("title"); // string | null
db.run([file!.path, file!.mimeType, file!.size, title]);
Expand Down
12 changes: 6 additions & 6 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pnpm add @chttp/router
```

```ts
import { createRouter } from "@chttp/router";
import { CtxCore, createRouter } from "@chttp/router";

const app = createRouter();

Expand All @@ -16,18 +16,18 @@ function sleep(ms: number): Promise<void> {
}

// Sync handler: returns immediately, no microtask cost.
app.get("/", (c) => c.text("hi"));
app.get("/user/:id", (c) => c.json({ id: c.req.param("id") }));
app.get("/", (c: CtxCore) => c.text("hi"));
app.get("/user/:id", (c: CtxCore) => c.json({ id: c.req.param("id") }));

// Async handler: await anything, the router waits for it to settle.
app.get("/slow", async (c) => {
app.get("/slow", async (c: CtxCore) => {
await sleep(100);
c.text("done");
});

// Declare { body: true } or c.req.body() returns "".
app.post("/echo", (c) => c.text(c.req.body()), { body: true });
app.notFound((c) => c.status(404).text("not found"));
app.post("/echo", (c: CtxCore) => c.text(c.req.body()), { body: true });
app.notFound((c: CtxCore) => c.status(404).text("not found"));

app.serve({ port: 3000 });
```
Expand Down
6 changes: 4 additions & 2 deletions docs/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ Routes are registered per method: `app.get | post | put | delete | patch | head
Paths support static segments, `:param`, and a tail `*` wildcard.

```ts
app.get("/user/:id", (c) => c.req.param("id")); // literal-typed: 'id'
app.get("/wild/*", (c) => c.req.param("*"));
import { CtxCore } from "@chttp/router";

app.get("/user/:id", (c: CtxCore) => c.req.param("id")); // literal-typed: 'id'
app.get("/wild/*", (c: CtxCore) => c.req.param("*"));
```

## Semantics
Expand Down
4 changes: 2 additions & 2 deletions docs/static.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
`@chttp/static` serves files and single page apps from a directory or from bytes baked into the binary. It depends on `@chttp/router` only and is authored in the scriptc subset, so it compiles into the same native ELF as the rest of your app.

```ts
import { createRouter } from "@chttp/router";
import { CtxCore, createRouter } from "@chttp/router";
import { DiskAssets, serveStatic } from "@chttp/static";

const app = createRouter();
app.get("/api/ping", (c) => c.json({ ok: true }));
app.get("/api/ping", (c: CtxCore) => c.json({ ok: true }));
serveStatic(app, "/", new DiskAssets("./web/dist"), { spa: true });
app.serve({ port: 3000 });
```
Expand Down
6 changes: 3 additions & 3 deletions docs/streaming.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
`@chttp/stream` adds Server-Sent Events and chunked text responses to `@chttp/router`. It is authored in the scriptc subset, so it compiles into the same native binary as your app.

```ts
import { createRouter } from "@chttp/router";
import { CtxCore, createRouter } from "@chttp/router";
import { streamSSE } from "@chttp/stream";

const app = createRouter();

app.get("/clock", (c) =>
app.get("/clock", (c: CtxCore) =>
streamSSE(c, (s) => {
const t = setInterval(() => s.send(new Date().toISOString(), "tick"), 1000);
s.onAbort(() => clearInterval(t));
Expand Down Expand Up @@ -43,7 +43,7 @@ Multi-line data is split on LF, CRLF or CR into one `data:` line each, as the SS
`streamText(c, fn)` is the same shape for `text/plain` chunked output. The stream has `write(s)`, `writeln(s)`, `close()` and `onAbort(fn)`.

```ts
app.get("/log", (c) =>
app.get("/log", (c: CtxCore) =>
streamText(c, (s) => {
s.writeln("starting");
s.writeln("done");
Expand Down
6 changes: 3 additions & 3 deletions docs/unplugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ Mark a routes module:

```ts
// src/routes.chttp.ts
import { createRouter } from '@chttp/router'
import { CtxCore, createRouter } from '@chttp/router'

export const app = createRouter()

app.get('/user/:id', (c) => c.json({ id: c.req.param('id') }))
app.post('/echo', (c) => c.text(c.req.body()), { body: true })
app.get('/user/:id', (c: CtxCore) => c.json({ id: c.req.param('id') }))
app.post('/echo', (c: CtxCore) => c.text(c.req.body()), { body: true })
```

Wire the generated dispatch:
Expand Down
5 changes: 3 additions & 2 deletions packages/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Every function is string-in/string-out or boolean-out; none of them touches a re
## Quickstart

```ts
import { CtxCore } from "@chttp/router";
import {
MemorySessionStore,
buildSessionCookie,
Expand All @@ -18,14 +19,14 @@ import {

const store = new MemorySessionStore(); // dev/test only — see below

app.post("/login", async (c) => {
app.post("/login", async (c: CtxCore) => {
const token = generateSessionToken();
await createSession(store, token, "user-1", { role: "admin" });
c.header("set-cookie", buildSessionCookie(token, { secure: false }));
return c.status(201).text("ok");
});

app.get("/me", async (c) => {
app.get("/me", async (c: CtxCore) => {
const tok = readCookie(c.req.header("cookie"), "session");
const rec = tok === null ? null : await validateSessionToken(store, tok);
if (rec === null) return c.status(401).text("unauthorized");
Expand Down
12 changes: 6 additions & 6 deletions packages/router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
Minimal async-first HTTP router for `node:http`, zero dependencies, authored inside the [scriptc](https://github.com/vercel-labs/scriptc) compilable subset. The same source lowers to a native ELF directly, no plugin required.

```ts
import { createRouter } from "@chttp/router";
import { CtxCore, createRouter } from "@chttp/router";

const app = createRouter();

app.get("/", (c) => c.text("hi"));
app.get("/user/:id", (c) => c.json({ id: c.req.param("id") }));
app.post("/echo", (c) => c.text(c.req.body()));
app.get("/", (c: CtxCore) => c.text("hi"));
app.get("/user/:id", (c: CtxCore) => c.json({ id: c.req.param("id") }));
app.post("/echo", (c: CtxCore) => c.text(c.req.body()));

app.get("/slow", async (c) => {
app.get("/slow", async (c: CtxCore) => {
const user = await fetchUser(c.req.param("id"));
return c.json(user);
});

app.notFound((c) => c.status(404).text("not found"));
app.notFound((c: CtxCore) => c.status(404).text("not found"));

app.serve({ port: 3000 });
```
Expand Down
4 changes: 2 additions & 2 deletions packages/stream/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
Server-Sent Events and chunked text streaming for [@chttp/router](https://github.com/sigilco/chttp), authored inside the scriptc compilable subset so it compiles into the same native binary.

```ts
import { createRouter } from "@chttp/router";
import { CtxCore, createRouter } from "@chttp/router";
import { streamSSE } from "@chttp/stream";

const app = createRouter();

app.get("/clock", (c) =>
app.get("/clock", (c: CtxCore) =>
streamSSE(c, (s) => {
const t = setInterval(() => s.send(new Date().toISOString(), "tick"), 1000);
s.onAbort(() => clearInterval(t));
Expand Down
Loading