@spikard/wasm 0.2.1

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/README.md ADDED
@@ -0,0 +1,719 @@
1
+ # spikard-wasm
2
+
3
+ WebAssembly bindings for Spikard HTTP framework via wasm-bindgen.
4
+
5
+ ## Status & Badges
6
+
7
+ [![Documentation](https://img.shields.io/badge/docs-spikard.dev-58FBDA)](https://spikard.dev)
8
+ [![npm](https://img.shields.io/npm/v/spikard-wasm.svg)](https://www.npmjs.com/package/spikard-wasm)
9
+ [![npm downloads](https://img.shields.io/npm/dm/spikard-wasm.svg)](https://www.npmjs.com/package/spikard-wasm)
10
+ [![Crates.io](https://img.shields.io/crates/v/spikard-wasm.svg)](https://crates.io/crates/spikard-wasm)
11
+ [![Documentation (Rust)](https://docs.rs/spikard-wasm/badge.svg)](https://docs.rs/spikard-wasm)
12
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
13
+ [![Discord](https://img.shields.io/badge/Discord-Join%20our%20community-7289da)](https://discord.gg/pXxagNK2zN)
14
+
15
+ ## Overview
16
+
17
+ Edge-friendly TypeScript web framework for WASM runtimes (Deno, Cloudflare Workers, browsers). Build REST APIs with the same routing primitives as spikard Node.js bindings, compiled to WebAssembly for maximum portability.
18
+
19
+ ## Installation
20
+
21
+ **From npm:**
22
+
23
+ ```bash
24
+ npm install spikard-wasm
25
+ # or
26
+ pnpm add spikard-wasm
27
+ # or
28
+ yarn add spikard-wasm
29
+ # or
30
+ deno add npm:spikard-wasm
31
+ ```
32
+
33
+ **From source:**
34
+
35
+ ```bash
36
+ cd packages/wasm
37
+ pnpm install
38
+ pnpm build # emits ESM to dist/
39
+ ```
40
+
41
+ **Requirements:**
42
+ - Node.js 20+ / Deno 1.40+ / Bun 1.0+
43
+ - For Cloudflare Workers: Wrangler 3+
44
+ - For browsers: Modern browser with WASM support
45
+
46
+ ## Quick Start
47
+
48
+ ### Cloudflare Workers
49
+
50
+ ```typescript
51
+ import { Spikard, get, post, createFetchHandler } from "spikard-wasm";
52
+ import { z } from "zod";
53
+
54
+ const app = new Spikard();
55
+
56
+ get("/hello")(async () => ({
57
+ message: "Hello from the edge!"
58
+ }));
59
+
60
+ const UserSchema = z.object({
61
+ name: z.string(),
62
+ email: z.string().email(),
63
+ });
64
+
65
+ post("/users", {
66
+ bodySchema: UserSchema
67
+ })(async (req) => {
68
+ const user = req.json<z.infer<typeof UserSchema>>();
69
+ return { id: 1, ...user };
70
+ });
71
+
72
+ export default {
73
+ fetch: createFetchHandler(app),
74
+ };
75
+ ```
76
+
77
+ ### Deno
78
+
79
+ ```typescript
80
+ import { Spikard, get } from "npm:spikard-wasm";
81
+
82
+ const app = new Spikard();
83
+
84
+ get("/")(async () => ({
85
+ message: "Hello from Deno!"
86
+ }));
87
+
88
+ Deno.serve({ port: 8000 }, (request) => {
89
+ return app.handleRequest(request);
90
+ });
91
+ ```
92
+
93
+ ### Browser
94
+
95
+ ```typescript
96
+ import { Spikard, get, TestClient } from "spikard-wasm";
97
+
98
+ const app = new Spikard();
99
+
100
+ get("/api/data")(async () => ({
101
+ timestamp: Date.now(),
102
+ data: [1, 2, 3],
103
+ }));
104
+
105
+ // Use TestClient for in-browser API calls
106
+ const client = new TestClient(app);
107
+ const response = await client.get("/api/data");
108
+ console.log(response.json());
109
+ ```
110
+
111
+ ## Route Registration
112
+
113
+ ### Decorator-Style Registration
114
+
115
+ Routes are registered using HTTP method decorators:
116
+
117
+ ```typescript
118
+ import { get, post, put, patch, del } from "spikard-wasm";
119
+
120
+ get("/users")(async () => {
121
+ return { users: [] };
122
+ });
123
+
124
+ post("/users")(async (req) => {
125
+ const user = req.json();
126
+ return { created: true, user };
127
+ });
128
+
129
+ put("/users/:id")(async (req) => {
130
+ const id = req.pathParams.id;
131
+ return { id, updated: true };
132
+ });
133
+
134
+ patch("/users/:id")(async (req) => {
135
+ return { id: req.pathParams.id, patched: true };
136
+ });
137
+
138
+ del("/users/:id")(async (req) => {
139
+ return { deleted: true };
140
+ });
141
+ ```
142
+
143
+ ### Manual Registration with `addRoute`
144
+
145
+ For dynamic route registration:
146
+
147
+ ```typescript
148
+ import { Spikard } from "spikard-wasm";
149
+
150
+ const app = new Spikard();
151
+
152
+ async function listUsers() {
153
+ return { users: [] };
154
+ }
155
+
156
+ app.addRoute(
157
+ {
158
+ method: "GET",
159
+ path: "/users",
160
+ handler_name: "listUsers",
161
+ is_async: true,
162
+ },
163
+ listUsers
164
+ );
165
+ ```
166
+
167
+ ### Supported HTTP Methods
168
+
169
+ - `GET` - Retrieve resources
170
+ - `POST` - Create resources
171
+ - `PUT` - Replace resources
172
+ - `PATCH` - Update resources
173
+ - `DELETE` - Delete resources
174
+ - `HEAD` - Get headers only
175
+ - `OPTIONS` - Get allowed methods
176
+ - `TRACE` - Echo the request
177
+
178
+ ### With Schemas
179
+
180
+ Spikard WASM supports **Zod schemas** and **raw JSON Schema objects**.
181
+
182
+ **With Zod (recommended - type inference):**
183
+
184
+ ```typescript
185
+ import { post } from "spikard-wasm";
186
+ import { z } from "zod";
187
+
188
+ const CreateUserSchema = z.object({
189
+ name: z.string().min(1),
190
+ email: z.string().email(),
191
+ age: z.number().int().min(18),
192
+ });
193
+
194
+ post("/users", {
195
+ bodySchema: CreateUserSchema,
196
+ responseSchema: z.object({ id: z.number(), name: z.string() }),
197
+ })(async function createUser(req) {
198
+ const user = req.json<z.infer<typeof CreateUserSchema>>();
199
+ return { id: 1, name: user.name };
200
+ });
201
+ ```
202
+
203
+ **With raw JSON Schema:**
204
+
205
+ ```typescript
206
+ const userSchema = {
207
+ type: "object",
208
+ properties: {
209
+ name: { type: "string" },
210
+ email: { type: "string", format: "email" },
211
+ },
212
+ required: ["name", "email"],
213
+ };
214
+
215
+ post("/users", { bodySchema: userSchema })(async function createUser(req) {
216
+ const user = req.json<{ name: string; email: string }>();
217
+ return { id: 1, ...user };
218
+ });
219
+ ```
220
+
221
+ ## Request Handling
222
+
223
+ ### Accessing Request Data
224
+
225
+ ```typescript
226
+ get("/search")(async function search(req) {
227
+ // Path parameters
228
+ const userId = req.pathParams.id;
229
+
230
+ // Query parameters
231
+ const params = new URLSearchParams(req.queryString);
232
+ const q = params.get("q");
233
+ const limit = params.get("limit") ?? "10";
234
+
235
+ // Headers
236
+ const auth = req.headers["authorization"];
237
+ const userAgent = req.headers["user-agent"];
238
+
239
+ // Cookies (if available)
240
+ const sessionId = req.cookies?.session_id;
241
+
242
+ // Method and path
243
+ console.log(`${req.method} ${req.path}`);
244
+
245
+ return { query: q, limit: parseInt(limit) };
246
+ });
247
+ ```
248
+
249
+ ### JSON Body
250
+
251
+ ```typescript
252
+ post("/users")(async function createUser(req) {
253
+ const body = req.json<{ name: string; email: string }>();
254
+ return { id: 1, ...body };
255
+ });
256
+ ```
257
+
258
+ ### Form Data
259
+
260
+ ```typescript
261
+ post("/login")(async function login(req) {
262
+ const form = req.form();
263
+ return {
264
+ username: form.username,
265
+ password: form.password,
266
+ };
267
+ });
268
+ ```
269
+
270
+ ## Handler Wrappers
271
+
272
+ For automatic parameter extraction:
273
+
274
+ ```typescript
275
+ import { wrapHandler, wrapBodyHandler } from "spikard-wasm";
276
+
277
+ interface CreateUserRequest {
278
+ name: string;
279
+ email: string;
280
+ }
281
+
282
+ // Body-only wrapper
283
+ post("/users", {}, wrapBodyHandler(async (body: CreateUserRequest) => {
284
+ return { id: 1, name: body.name };
285
+ }));
286
+
287
+ // Full context wrapper
288
+ get("/users/:id", {}, wrapHandler(async (params, query, body) => {
289
+ return { id: params.id, query };
290
+ }));
291
+ ```
292
+
293
+ ## File Uploads
294
+
295
+ ```typescript
296
+ import { UploadFile } from "spikard-wasm";
297
+
298
+ interface UploadRequest {
299
+ file: UploadFile;
300
+ description: string;
301
+ }
302
+
303
+ post("/upload")(async function upload(req) {
304
+ const body = req.json<UploadRequest>();
305
+ const content = body.file.read();
306
+
307
+ return {
308
+ filename: body.file.filename,
309
+ size: body.file.size,
310
+ contentType: body.file.contentType,
311
+ };
312
+ });
313
+ ```
314
+
315
+ ## Streaming Responses
316
+
317
+ ```typescript
318
+ import { StreamingResponse } from "spikard-wasm";
319
+
320
+ async function* generateData() {
321
+ for (let i = 0; i < 10; i++) {
322
+ yield JSON.stringify({ count: i }) + "\n";
323
+ await new Promise((resolve) => setTimeout(resolve, 100));
324
+ }
325
+ }
326
+
327
+ get("/stream")(async function stream() {
328
+ return new StreamingResponse(generateData(), {
329
+ statusCode: 200,
330
+ headers: { "Content-Type": "application/x-ndjson" },
331
+ });
332
+ });
333
+ ```
334
+
335
+ ### Server-Sent Events (SSE)
336
+
337
+ ```typescript
338
+ get("/events")(async function events() {
339
+ async function* sseGenerator() {
340
+ for (let i = 0; i < 10; i++) {
341
+ yield `data: ${JSON.stringify({ count: i })}\n\n`;
342
+ await new Promise((resolve) => setTimeout(resolve, 1000));
343
+ }
344
+ }
345
+
346
+ return new StreamingResponse(sseGenerator(), {
347
+ statusCode: 200,
348
+ headers: {
349
+ "Content-Type": "text/event-stream",
350
+ "Cache-Control": "no-cache",
351
+ "Connection": "keep-alive",
352
+ },
353
+ });
354
+ });
355
+ ```
356
+
357
+ ## Configuration
358
+
359
+ ```typescript
360
+ import { Spikard, type ServerConfig } from "spikard-wasm";
361
+
362
+ const app = new Spikard();
363
+
364
+ const config: ServerConfig = {
365
+ enableRequestId: true,
366
+ maxBodySize: 10 * 1024 * 1024, // 10 MB
367
+ requestTimeout: 30, // seconds
368
+ compression: {
369
+ gzip: true,
370
+ brotli: true,
371
+ quality: 9,
372
+ minSize: 1024,
373
+ },
374
+ rateLimit: {
375
+ perSecond: 100,
376
+ burst: 200,
377
+ ipBased: true,
378
+ },
379
+ cors: {
380
+ allowOrigins: ["*"],
381
+ allowMethods: ["GET", "POST", "PUT", "DELETE"],
382
+ allowHeaders: ["Content-Type", "Authorization"],
383
+ maxAge: 86400,
384
+ },
385
+ openapi: {
386
+ enabled: true,
387
+ title: "Edge API",
388
+ version: "1.0.0",
389
+ },
390
+ };
391
+
392
+ // Apply configuration
393
+ app.configure(config);
394
+ ```
395
+
396
+ ## Lifecycle Hooks
397
+
398
+ ```typescript
399
+ app.onRequest(async (request) => {
400
+ console.log(`${request.method} ${request.path}`);
401
+ return request;
402
+ });
403
+
404
+ app.preValidation(async (request) => {
405
+ // Check before validation
406
+ if (!request.headers["authorization"]) {
407
+ return {
408
+ status: 401,
409
+ body: { error: "Unauthorized" },
410
+ };
411
+ }
412
+ return request;
413
+ });
414
+
415
+ app.preHandler(async (request) => {
416
+ // After validation, before handler
417
+ request.startTime = Date.now();
418
+ return request;
419
+ });
420
+
421
+ app.onResponse(async (response) => {
422
+ response.headers["X-Frame-Options"] = "DENY";
423
+ response.headers["X-Content-Type-Options"] = "nosniff";
424
+ return response;
425
+ });
426
+
427
+ app.onError(async (response) => {
428
+ console.error(`Error: ${response.status}`);
429
+ return response;
430
+ });
431
+ ```
432
+
433
+ ## Testing
434
+
435
+ ### In-Memory Test Client
436
+
437
+ ```typescript
438
+ import { TestClient } from "spikard-wasm";
439
+ import { expect } from "vitest";
440
+
441
+ const app = new Spikard();
442
+
443
+ get("/users/:id")(async (req) => {
444
+ return { id: req.pathParams.id, name: "Alice" };
445
+ });
446
+
447
+ const client = new TestClient(app);
448
+
449
+ const response = await client.get("/users/123");
450
+ expect(response.statusCode).toBe(200);
451
+ expect(response.json()).toEqual({ id: "123", name: "Alice" });
452
+ ```
453
+
454
+ ### WebSocket Testing
455
+
456
+ ```typescript
457
+ import { ws } from "spikard-wasm";
458
+
459
+ ws("/ws")(async (socket) => {
460
+ socket.on("message", (msg) => {
461
+ socket.send({ echo: msg });
462
+ });
463
+ });
464
+
465
+ const client = new TestClient(app);
466
+ const ws = await client.websocketConnect("/ws");
467
+ await ws.sendJson({ message: "hello" });
468
+ const response = await ws.receiveJson();
469
+ expect(response.echo.message).toBe("hello");
470
+ await ws.close();
471
+ ```
472
+
473
+ ### SSE Testing
474
+
475
+ ```typescript
476
+ const response = await client.get("/events");
477
+ const sse = new SseStream(response.text());
478
+ const events = sse.eventsAsJson();
479
+ expect(events.length).toBeGreaterThan(0);
480
+ ```
481
+
482
+ ## Type Safety
483
+
484
+ Full TypeScript support with auto-generated types:
485
+
486
+ ```typescript
487
+ import {
488
+ type Request,
489
+ type Response,
490
+ type ServerConfig,
491
+ type RouteOptions,
492
+ type HandlerFunction,
493
+ } from "spikard-wasm";
494
+ ```
495
+
496
+ ### Parameter Types
497
+
498
+ ```typescript
499
+ import { Query, Path, Body, QueryDefault } from "spikard-wasm";
500
+
501
+ function handler(
502
+ id: Path<number>,
503
+ limit: Query<string | undefined>,
504
+ body: Body<UserType>
505
+ ) {
506
+ // Full type inference
507
+ }
508
+ ```
509
+
510
+ ## Validation with Zod
511
+
512
+ ```typescript
513
+ import { z } from "zod";
514
+
515
+ const UserSchema = z.object({
516
+ name: z.string().min(1).max(100),
517
+ email: z.string().email(),
518
+ age: z.number().int().min(18).optional(),
519
+ tags: z.array(z.string()).default([]),
520
+ });
521
+
522
+ post("/users", { bodySchema: UserSchema })(async function createUser(req) {
523
+ const user = req.json<z.infer<typeof UserSchema>>();
524
+ // user is fully typed and validated
525
+ return user;
526
+ });
527
+ ```
528
+
529
+ ## Performance
530
+
531
+ WASM bindings provide:
532
+ - **WebAssembly compilation** for near-native performance
533
+ - **Zero-copy data structures** where supported by runtime
534
+ - **Shared memory optimization** for large payloads
535
+ - **Streaming support** for efficient data transfer
536
+ - **Tree-shakable ESM** for minimal bundle sizes
537
+
538
+ ### Bundle Size Optimization
539
+
540
+ ```typescript
541
+ // Import only what you need
542
+ import { get, post } from "spikard-wasm/routing";
543
+ import { TestClient } from "spikard-wasm/testing";
544
+ ```
545
+
546
+ ## Platform-Specific Examples
547
+
548
+ ### Cloudflare Workers
549
+
550
+ ```typescript
551
+ import { Spikard, get, createFetchHandler } from "spikard-wasm";
552
+
553
+ const app = new Spikard();
554
+
555
+ get("/")(async (req) => {
556
+ return {
557
+ message: "Hello from Cloudflare Workers",
558
+ cf: req.cf, // Cloudflare-specific properties
559
+ };
560
+ });
561
+
562
+ export default {
563
+ fetch: createFetchHandler(app),
564
+ };
565
+ ```
566
+
567
+ ### Deno Deploy
568
+
569
+ ```typescript
570
+ import { Spikard, get } from "npm:spikard-wasm";
571
+
572
+ const app = new Spikard();
573
+
574
+ get("/")(async () => ({ message: "Hello from Deno Deploy" }));
575
+
576
+ Deno.serve(
577
+ { port: 8000 },
578
+ (request: Request) => app.handleRequest(request)
579
+ );
580
+ ```
581
+
582
+ ### Vercel Edge Functions
583
+
584
+ ```typescript
585
+ import { Spikard, get, createFetchHandler } from "spikard-wasm";
586
+
587
+ const app = new Spikard();
588
+
589
+ get("/api/hello")(async () => ({ message: "Hello from Vercel Edge" }));
590
+
591
+ export const config = { runtime: "edge" };
592
+ export default createFetchHandler(app);
593
+ ```
594
+
595
+ ### Browser (Service Worker)
596
+
597
+ ```typescript
598
+ import { Spikard, get } from "spikard-wasm";
599
+
600
+ const app = new Spikard();
601
+
602
+ get("/api/data")(async () => ({
603
+ cached: true,
604
+ timestamp: Date.now(),
605
+ }));
606
+
607
+ self.addEventListener("fetch", (event) => {
608
+ if (event.request.url.includes("/api/")) {
609
+ event.respondWith(app.handleRequest(event.request));
610
+ }
611
+ });
612
+ ```
613
+
614
+ ## Code Generation
615
+
616
+ Generate type-safe WASM applications from OpenAPI/AsyncAPI specs:
617
+
618
+ ```bash
619
+ # Generate from OpenAPI
620
+ spikard generate openapi \
621
+ --fixtures ../../testing_data \
622
+ --output ./generated \
623
+ --target wasm
624
+
625
+ # Generate from AsyncAPI
626
+ spikard generate asyncapi \
627
+ --fixtures ../../testing_data/websockets \
628
+ --output ./generated \
629
+ --target wasm
630
+ ```
631
+
632
+ ## Examples
633
+
634
+ See `/examples/wasm/` for more examples:
635
+ - **Basic REST API** - Simple CRUD operations
636
+ - **Cloudflare Workers** - Edge deployment
637
+ - **Deno Deploy** - Deno-specific features
638
+ - **WebSocket Chat** - Real-time communication
639
+ - **SSE Dashboard** - Server-sent events
640
+ - **File Upload** - Multipart form handling
641
+
642
+ ## Development Notes
643
+
644
+ ### Building from Source
645
+
646
+ ```bash
647
+ # Install dependencies
648
+ pnpm install
649
+
650
+ # Build WASM module
651
+ cd crates/spikard-wasm
652
+ wasm-pack build --target web
653
+
654
+ # Build TypeScript wrapper
655
+ cd ../../packages/wasm
656
+ pnpm build
657
+ ```
658
+
659
+ ### Running Tests
660
+
661
+ ```bash
662
+ # Run all tests
663
+ pnpm test
664
+
665
+ # Run specific test file
666
+ pnpm test -- routing.spec.ts
667
+
668
+ # Run with coverage
669
+ pnpm test:coverage
670
+ ```
671
+
672
+ ### Debugging WASM
673
+
674
+ Enable WASM debugging in your browser:
675
+ 1. Open DevTools
676
+ 2. Enable "WebAssembly Debugging" in Experiments
677
+ 3. Reload the page
678
+ 4. Set breakpoints in WASM code
679
+
680
+ ## Differences from Node.js Bindings
681
+
682
+ ### What's the Same
683
+ - Routing API (same decorators and methods)
684
+ - Request/Response types
685
+ - Validation with Zod/JSON Schema
686
+ - Lifecycle hooks
687
+ - Test client API
688
+
689
+ ### What's Different
690
+ - **No native modules** - Pure WASM, no Node.js addons
691
+ - **Fetch API only** - No Node.js `http` module
692
+ - **Smaller bundle** - Tree-shakable ESM exports
693
+ - **Platform-agnostic** - Works in browsers, Deno, Workers
694
+ - **Edge-optimized** - Designed for edge runtimes
695
+
696
+ ### When to Use WASM vs Node.js
697
+
698
+ **Use WASM bindings when:**
699
+ - Deploying to edge runtimes (Cloudflare, Vercel, Deno Deploy)
700
+ - Running in browsers or service workers
701
+ - Need maximum portability across platforms
702
+ - Want smallest possible bundle size
703
+
704
+ **Use Node.js bindings when:**
705
+ - Running on traditional Node.js servers
706
+ - Need native performance (napi-rs is ~10% faster)
707
+ - Using Node.js-specific features (file system, child processes)
708
+ - Maximum throughput is critical
709
+
710
+ ## Documentation
711
+
712
+ - [Main Project README](../../README.md)
713
+ - [Contributing Guide](../../CONTRIBUTING.md)
714
+ - [TypeScript API Reference](./src/index.ts)
715
+ - [Architecture Decision Records](../../docs/adr/)
716
+
717
+ ## License
718
+
719
+ MIT