@fresho/router 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 fresho.dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,45 +1,418 @@
1
1
  # @fresho/router
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ Type-safe routing for Cloudflare Workers, Deno, Bun, and Node.js. Define routes once, get validated handlers, typed clients, and OpenAPI docs.
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ **~2KB gzipped. Zero dependencies.**
6
6
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
7
+ ```typescript
8
+ import { route, router } from "@fresho/router";
8
9
 
9
- ## Purpose
10
+ const api = router({
11
+ health: router({
12
+ get: async () => ({ status: "ok" }),
13
+ }),
10
14
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@fresho/router`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
15
+ users: router({
16
+ // GET /users - list with optional limit
17
+ get: route({
18
+ query: { limit: "number?" },
19
+ handler: async (c) => db.users.list(c.query.limit),
20
+ }),
15
21
 
16
- ## What is OIDC Trusted Publishing?
22
+ // POST /users - create user
23
+ post: route({
24
+ body: { name: "string", email: "string" },
25
+ handler: async (c) => db.users.create(c.body),
26
+ }),
17
27
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
28
+ // GET /users/:id - get by id
29
+ $id: router({
30
+ // Bare function shorthand instead of `route` type
31
+ get: async (c) => db.users.get(c.path.id),
32
+ }),
33
+ }),
34
+ });
19
35
 
20
- ## Setup Instructions
36
+ // Cloudflare Worker / Deno / Bun
37
+ export default { fetch: api.handler() };
38
+ ```
21
39
 
22
- To properly configure OIDC trusted publishing for this package:
40
+ ## Features
23
41
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
42
+ - **Type-safe path params** — `$id` creates dynamic segments, typed via `route.ctx<>()`
43
+ - **Schema validation** — query and body validated at runtime, typed at compile time
44
+ - **Property-based routing** — property names become URL segments
45
+ - **Typed HTTP client** — call your API with full type safety
46
+ - **Typed local client** — test handlers directly without HTTP
47
+ - **OpenAPI generation** — auto-generate docs from your routes
48
+ - **Middleware** — cors, auth, rate limiting, and more
49
+ - **Streaming** — SSE and JSON lines helpers
50
+ - **HTTP-compliant** — automatic HEAD support for GET routes
51
+ - **Zero dependencies** — just Web APIs
28
52
 
29
- ## DO NOT USE THIS PACKAGE
53
+ ## Installation
30
54
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
55
+ ```bash
56
+ npm install @fresho/router
57
+ ```
36
58
 
37
- ## More Information
59
+ ## Path Convention
38
60
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
61
+ Property names become URL path segments:
42
62
 
43
- ---
63
+ ```typescript
64
+ router({
65
+ api: router({ // /api
66
+ users: router({ // /api/users
67
+ get: async () => ...,
68
+ $id: router({ // /api/users/:id
69
+ get: async (c) => c.path.id,
70
+ }),
71
+ }),
72
+ }),
73
+ });
74
+ ```
44
75
 
45
- **Maintained for OIDC setup purposes only**
76
+ - Regular properties → static segments (`users` → `/users`)
77
+ - `$param` properties → dynamic segments (`$id` → `/:id`)
78
+ - HTTP methods (`get`, `post`, etc.) → handlers at that path
79
+
80
+ ## Schemas
81
+
82
+ Define query and body schemas using shorthand syntax:
83
+
84
+ ```typescript
85
+ // Primitives
86
+ { name: 'string' } // required string
87
+ { age: 'number' } // required number (coerced from string in query)
88
+ { active: 'boolean' } // required boolean (accepts "true"/"false"/"1"/"0")
89
+
90
+ // Optional (append ?)
91
+ { name: 'string?' } // optional string
92
+
93
+ // Arrays
94
+ { tags: 'string[]' } // string array
95
+ { scores: 'number[]' } // number array
96
+
97
+ // Nested objects
98
+ {
99
+ address: {
100
+ street: 'string',
101
+ city: 'string',
102
+ zip: 'number?'
103
+ }
104
+ }
105
+ ```
106
+
107
+ Types are automatically inferred:
108
+
109
+ ```typescript
110
+ post: route({
111
+ body: {
112
+ title: "string",
113
+ tags: "string[]",
114
+ metadata: { priority: "number", draft: "boolean?" },
115
+ },
116
+ handler: async (c) => {
117
+ c.body.title; // string
118
+ c.body.tags; // string[]
119
+ c.body.metadata; // { priority: number, draft: boolean | undefined }
120
+ },
121
+ });
122
+ ```
123
+
124
+ ## Typing Best Practices
125
+
126
+ **Schemas** (`query`/`body`) provide runtime validation AND type inference:
127
+
128
+ ```typescript
129
+ get: route({
130
+ query: { limit: "number?" },
131
+ handler: async (c) => c.query.limit, // number | undefined
132
+ });
133
+ ```
134
+
135
+ **Context** (`route.ctx<T>()`) provides types only for things schemas don't cover:
136
+
137
+ ```typescript
138
+ interface MyContext {
139
+ path: { id: string }; // from $id segment
140
+ env: { DB: Database }; // runtime environment
141
+ user: { name: string }; // from auth middleware
142
+ }
143
+
144
+ get: route.ctx<MyContext>()({
145
+ query: { include: "string?" }, // validated
146
+ handler: async (c) => ({
147
+ id: c.path.id, // from context
148
+ user: c.user.name, // from context
149
+ include: c.query.include, // from schema
150
+ }),
151
+ });
152
+ ```
153
+
154
+ **Don't add explicit type annotations to handlers** — let types flow from schemas and context:
155
+
156
+ ```typescript
157
+ // GOOD: types inferred
158
+ handler: async (c) => c.query.limit;
159
+
160
+ // BAD: redundant annotation
161
+ handler: async (c: { query: { limit?: number } }) => c.query.limit;
162
+ ```
163
+
164
+ ## Nested Routers
165
+
166
+ Compose routers:
167
+
168
+ ```typescript
169
+ const users = router({
170
+ get: async () => db.users.list(),
171
+ post: route({
172
+ body: { name: 'string' },
173
+ handler: async (c) => db.users.create(c.body),
174
+ }),
175
+ $id: router({
176
+ get: async (c) => db.users.get(c.path.id),
177
+ delete: async (c) => db.users.delete(c.path.id),
178
+ }),
179
+ });
180
+
181
+ const api = router({
182
+ users,
183
+ posts: router({ ... }),
184
+ });
185
+
186
+ // Routes:
187
+ // GET /users
188
+ // POST /users
189
+ // GET /users/:id
190
+ // DELETE /users/:id
191
+ ```
192
+
193
+ ## Middleware
194
+
195
+ Add middleware to routers:
196
+
197
+ ```typescript
198
+ import { router, route } from "@fresho/router";
199
+ import { cors, errorHandler, jwtAuth } from "@fresho/router/middleware";
200
+
201
+ const api = router(
202
+ {
203
+ hello: router({
204
+ get: async () => ({ message: "world" }),
205
+ }),
206
+ },
207
+ cors(),
208
+ errorHandler(),
209
+ jwtAuth({ secret: process.env.JWT_SECRET, claims: (p) => ({ user: p.sub }) })
210
+ );
211
+ ```
212
+
213
+ Built-in middleware: `cors`, `errorHandler`, `logger`, `rateLimit`, `requestId`, `timeout`, `basicAuth`, `bearerAuth`, `jwtAuth`, `contentType`.
214
+
215
+ See **[Middleware Documentation](docs/middleware.md)** for detailed usage.
216
+
217
+ ## HTTP Client
218
+
219
+ Generate a typed client for your API:
220
+
221
+ ```typescript
222
+ // === Server (api.ts) ===
223
+ import { route, router } from "@fresho/router";
224
+
225
+ export const api = router({
226
+ users: router({
227
+ get: route({
228
+ query: { limit: "number?" },
229
+ handler: async (c) => ({ users: [], limit: c.query.limit }),
230
+ }),
231
+ $id: router({
232
+ get: async (c) => ({ id: c.path.id, name: "Alice" }),
233
+ }),
234
+ }),
235
+ });
236
+
237
+ // === Client ===
238
+ import { createHttpClient } from "@fresho/router";
239
+ import type { api } from "./api"; // Type-only import!
240
+
241
+ const client = createHttpClient<typeof api>({
242
+ baseUrl: "https://api.example.com",
243
+ });
244
+
245
+ // Direct call for implicit GET
246
+ const users = await client.users({ query: { limit: 10 } });
247
+
248
+ // Path params
249
+ const user = await client.users.$id({ path: { id: "123" } });
250
+ // user is typed as { id: string, name: string }
251
+
252
+ // Explicit HTTP methods use $-prefix
253
+ await client.users.$get(); // GET /users
254
+ await client.users.$post({ body: { name: "Bob" } }); // POST /users
255
+ ```
256
+
257
+ ### HTTP Method Syntax
258
+
259
+ HTTP methods are prefixed with `$` to distinguish them from path navigation:
260
+
261
+ ```typescript
262
+ // $-prefixed = execute HTTP method
263
+ client.users.$get(); // GET /users
264
+ client.users.$post({}); // POST /users
265
+ client.users.$put({}); // PUT /users
266
+ client.users.$delete(); // DELETE /users
267
+
268
+ // No prefix = navigate to path segment
269
+ client.api.get.$get(); // GET /api/get (navigate to "get", then execute GET)
270
+ ```
271
+
272
+ This allows routes with path segments named after HTTP methods (e.g., `/api/get`, `/resources/delete`).
273
+
274
+ ## Local Client
275
+
276
+ Test handlers directly without HTTP overhead:
277
+
278
+ ```typescript
279
+ import { createLocalClient } from "@fresho/router";
280
+
281
+ const client = createLocalClient(api);
282
+ client.configure({ env: { DB: mockDb } });
283
+
284
+ const user = await client.users.$id({ path: { id: "123" } });
285
+ assert.equal(user.name, "Alice");
286
+ ```
287
+
288
+ ## OpenAPI Generation
289
+
290
+ Generate OpenAPI 3.0 documentation:
291
+
292
+ ```typescript
293
+ import { generateDocs } from "@fresho/router";
294
+
295
+ const spec = generateDocs(api, {
296
+ title: "My API",
297
+ version: "1.0.0",
298
+ });
299
+
300
+ // Serve at /openapi.json
301
+ const docs = router({
302
+ openapi: router({
303
+ get: async () => spec,
304
+ }),
305
+ });
306
+ ```
307
+
308
+ ## Streaming
309
+
310
+ ### Server-Sent Events
311
+
312
+ ```typescript
313
+ import { sseResponse } from "@fresho/router";
314
+
315
+ events: router({
316
+ get: async () =>
317
+ sseResponse(async (send, close) => {
318
+ send({ data: "connected" });
319
+ send({ event: "update", data: { count: 1 } });
320
+ close();
321
+ }),
322
+ });
323
+ ```
324
+
325
+ ### JSON Lines
326
+
327
+ ```typescript
328
+ import { streamJsonLines } from "@fresho/router";
329
+
330
+ logs: router({
331
+ get: async () =>
332
+ streamJsonLines(async (send, close) => {
333
+ send({ level: "info", message: "Starting..." });
334
+ send({ level: "info", message: "Done" });
335
+ close();
336
+ }),
337
+ });
338
+ ```
339
+
340
+ ## Cloudflare Workers
341
+
342
+ ```typescript
343
+ import { route, router } from '@fresho/router';
344
+
345
+ const api = router({ ... });
346
+
347
+ export default {
348
+ fetch: api.handler(),
349
+ };
350
+ ```
351
+
352
+ ### Typed Context
353
+
354
+ Use `route.ctx<T>()` for environment bindings and middleware-added properties:
355
+
356
+ ```typescript
357
+ interface AppContext {
358
+ env: { DB: D1Database };
359
+ user: { id: string }; // from auth middleware
360
+ }
361
+
362
+ const api = router(
363
+ {
364
+ profile: router({
365
+ get: route.ctx<AppContext>()({
366
+ handler: async (c) => {
367
+ const data = await c.env.DB.prepare("...").all();
368
+ return { userId: c.user.id, data };
369
+ },
370
+ }),
371
+ }),
372
+ },
373
+ jwtAuth({
374
+ secret: (c) => c.env.JWT_SECRET,
375
+ claims: (p) => ({ user: { id: p.sub } }),
376
+ })
377
+ );
378
+
379
+ export default { fetch: api.handler() };
380
+ ```
381
+
382
+ ## HEAD Requests
383
+
384
+ Per [RFC 9110](https://httpwg.org/specs/rfc9110.html#HEAD), HEAD requests are automatically handled for any GET route:
385
+
386
+ ```typescript
387
+ users: router({
388
+ get: async () => ({ users: await db.getUsers() }),
389
+ });
390
+
391
+ // GET /users → 200 with body
392
+ // HEAD /users → 200 with no body (same headers)
393
+ ```
394
+
395
+ Define an explicit `head` handler if you need different behavior:
396
+
397
+ ```typescript
398
+ users: router({
399
+ head: async () => new Response(null, { headers: { "X-Count": "100" } }),
400
+ get: async () => ({ users: await db.getUsers() }),
401
+ });
402
+ ```
403
+
404
+ ## Size
405
+
406
+ | Usage | Minified | Gzipped |
407
+ | --------------------------- | -------- | ------- |
408
+ | Core (routing + validation) | 4.2 KB | 1.9 KB |
409
+ | + HTTP client | 5.8 KB | 2.4 KB |
410
+ | + OpenAPI docs | 5.3 KB | 2.3 KB |
411
+ | + cors, errorHandler | 6.5 KB | 2.7 KB |
412
+ | + all middleware | 11.6 KB | 4.4 KB |
413
+
414
+ Tree-shakeable: only pay for what you import.
415
+
416
+ ## License
417
+
418
+ MIT