@spikard/wasm 0.6.2 → 0.7.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.
@@ -1,739 +0,0 @@
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
- ### Benchmark Results
532
-
533
- Latest comparative run (2025-12-20, commit `25e4fdf`, Linux x86_64, AMD EPYC 7763 2c/4t, 50 concurrency, 10s, oha). Full artifacts: `snapshots/benchmarks/20397054933`.
534
-
535
- | Binding | Avg RPS (all workloads) | Avg latency (ms) |
536
- | --- | --- | --- |
537
- | spikard-rust | 55,755 | 1.00 |
538
- | spikard-node | 24,283 | 2.22 |
539
- | spikard-php | 20,176 | 2.66 |
540
- | spikard-python | 11,902 | 4.41 |
541
- | **spikard-wasm** | **10,658** | **5.70** |
542
- | spikard-ruby | 8,271 | 6.50 |
543
-
544
- WASM bindings deliver solid edge runtime performance—ideal for serverless platforms (Cloudflare Workers, Vercel Edge, Deno Deploy) where startup time and memory efficiency matter more than absolute throughput. Trade-offs vs Node.js bindings:
545
- - **Advantages:** Portable across all runtimes, smaller bundle size, cold start friendly
546
- - **Trade-offs:** ~2.3x lower RPS on traditional servers vs native napi-rs bindings
547
-
548
- ### Runtime Characteristics
549
-
550
- WASM bindings provide:
551
- - **WebAssembly compilation** for near-native performance in edge runtimes
552
- - **Zero-copy data structures** where supported by runtime
553
- - **Shared memory optimization** for large payloads
554
- - **Streaming support** for efficient data transfer
555
- - **Tree-shakable ESM** for minimal bundle sizes
556
- - **Multi-runtime support:** Cloudflare Workers, Deno Deploy, Vercel Edge, browsers, Node.js
557
-
558
- ### Bundle Size Optimization
559
-
560
- ```typescript
561
- // Import only what you need
562
- import { get, post } from "spikard-wasm/routing";
563
- import { TestClient } from "spikard-wasm/testing";
564
- ```
565
-
566
- ## Platform-Specific Examples
567
-
568
- ### Cloudflare Workers
569
-
570
- ```typescript
571
- import { Spikard, get, createFetchHandler } from "spikard-wasm";
572
-
573
- const app = new Spikard();
574
-
575
- get("/")(async (req) => {
576
- return {
577
- message: "Hello from Cloudflare Workers",
578
- cf: req.cf, // Cloudflare-specific properties
579
- };
580
- });
581
-
582
- export default {
583
- fetch: createFetchHandler(app),
584
- };
585
- ```
586
-
587
- ### Deno Deploy
588
-
589
- ```typescript
590
- import { Spikard, get } from "npm:spikard-wasm";
591
-
592
- const app = new Spikard();
593
-
594
- get("/")(async () => ({ message: "Hello from Deno Deploy" }));
595
-
596
- Deno.serve(
597
- { port: 8000 },
598
- (request: Request) => app.handleRequest(request)
599
- );
600
- ```
601
-
602
- ### Vercel Edge Functions
603
-
604
- ```typescript
605
- import { Spikard, get, createFetchHandler } from "spikard-wasm";
606
-
607
- const app = new Spikard();
608
-
609
- get("/api/hello")(async () => ({ message: "Hello from Vercel Edge" }));
610
-
611
- export const config = { runtime: "edge" };
612
- export default createFetchHandler(app);
613
- ```
614
-
615
- ### Browser (Service Worker)
616
-
617
- ```typescript
618
- import { Spikard, get } from "spikard-wasm";
619
-
620
- const app = new Spikard();
621
-
622
- get("/api/data")(async () => ({
623
- cached: true,
624
- timestamp: Date.now(),
625
- }));
626
-
627
- self.addEventListener("fetch", (event) => {
628
- if (event.request.url.includes("/api/")) {
629
- event.respondWith(app.handleRequest(event.request));
630
- }
631
- });
632
- ```
633
-
634
- ## Code Generation
635
-
636
- Generate type-safe WASM applications from OpenAPI/AsyncAPI specs:
637
-
638
- ```bash
639
- # Generate from OpenAPI
640
- spikard generate openapi \
641
- --fixtures ../../testing_data \
642
- --output ./generated \
643
- --target wasm
644
-
645
- # Generate from AsyncAPI
646
- spikard generate asyncapi \
647
- --fixtures ../../testing_data/websockets \
648
- --output ./generated \
649
- --target wasm
650
- ```
651
-
652
- ## Examples
653
-
654
- See `/examples/wasm/` for more examples:
655
- - **Basic REST API** - Simple CRUD operations
656
- - **Cloudflare Workers** - Edge deployment
657
- - **Deno Deploy** - Deno-specific features
658
- - **WebSocket Chat** - Real-time communication
659
- - **SSE Dashboard** - Server-sent events
660
- - **File Upload** - Multipart form handling
661
-
662
- ## Development Notes
663
-
664
- ### Building from Source
665
-
666
- ```bash
667
- # Install dependencies
668
- pnpm install
669
-
670
- # Build WASM module
671
- cd crates/spikard-wasm
672
- wasm-pack build --target web
673
-
674
- # Build TypeScript wrapper
675
- cd ../../packages/wasm
676
- pnpm build
677
- ```
678
-
679
- ### Running Tests
680
-
681
- ```bash
682
- # Run all tests
683
- pnpm test
684
-
685
- # Run specific test file
686
- pnpm test -- routing.spec.ts
687
-
688
- # Run with coverage
689
- pnpm test:coverage
690
- ```
691
-
692
- ### Debugging WASM
693
-
694
- Enable WASM debugging in your browser:
695
- 1. Open DevTools
696
- 2. Enable "WebAssembly Debugging" in Experiments
697
- 3. Reload the page
698
- 4. Set breakpoints in WASM code
699
-
700
- ## Differences from Node.js Bindings
701
-
702
- ### What's the Same
703
- - Routing API (same decorators and methods)
704
- - Request/Response types
705
- - Validation with Zod/JSON Schema
706
- - Lifecycle hooks
707
- - Test client API
708
-
709
- ### What's Different
710
- - **No native modules** - Pure WASM, no Node.js addons
711
- - **Fetch API only** - No Node.js `http` module
712
- - **Smaller bundle** - Tree-shakable ESM exports
713
- - **Platform-agnostic** - Works in browsers, Deno, Workers
714
- - **Edge-optimized** - Designed for edge runtimes
715
-
716
- ### When to Use WASM vs Node.js
717
-
718
- **Use WASM bindings when:**
719
- - Deploying to edge runtimes (Cloudflare, Vercel, Deno Deploy)
720
- - Running in browsers or service workers
721
- - Need maximum portability across platforms
722
- - Want smallest possible bundle size
723
-
724
- **Use Node.js bindings when:**
725
- - Running on traditional Node.js servers
726
- - Need native performance (napi-rs is ~10% faster)
727
- - Using Node.js-specific features (file system, child processes)
728
- - Maximum throughput is critical
729
-
730
- ## Documentation
731
-
732
- - [Main Project README](../../README.md)
733
- - [Contributing Guide](../../CONTRIBUTING.md)
734
- - [TypeScript API Reference](./src/index.ts)
735
- - [Architecture Decision Records](../../docs/adr/)
736
-
737
- ## License
738
-
739
- MIT