@spikard/wasm 0.6.2 → 0.7.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/README.md CHANGED
@@ -1,74 +1,72 @@
1
- # spikard-wasm
1
+ # @spikard/wasm
2
2
 
3
- WebAssembly bindings for Spikard HTTP framework via wasm-bindgen.
3
+ > **Note:** As of v0.2.1, this package has moved to `@spikard/wasm`. Update your imports from `'spikard-wasm'` to `'@spikard/wasm'`. See [MIGRATION-0.2.1.md](../../MIGRATION-0.2.1.md) for details.
4
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)
5
+ [![npm](https://img.shields.io/npm/v/@spikard/wasm.svg)](https://www.npmjs.com/package/@spikard/wasm)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@spikard/wasm.svg)](https://www.npmjs.com/package/@spikard/wasm)
12
7
  [![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.
8
+ [![codecov](https://codecov.io/gh/Goldziher/spikard/graph/badge.svg?token=H4ZXDZ4A69)](https://codecov.io/gh/Goldziher/spikard)
9
+ [![PyPI](https://img.shields.io/pypi/v/spikard.svg)](https://pypi.org/project/spikard/)
10
+ [![Crates.io](https://img.shields.io/crates/v/spikard.svg)](https://crates.io/crates/spikard)
11
+ [![RubyGems](https://img.shields.io/gem/v/spikard.svg)](https://rubygems.org/gems/spikard)
12
+ [![Packagist](https://img.shields.io/packagist/v/spikard/spikard.svg)](https://packagist.org/packages/spikard/spikard)
13
+
14
+ Spikard HTTP framework compiled to **WebAssembly with full TypeScript support** for edge runtimes, browsers, and server-side JavaScript environments. Build type-safe web services that run anywhere.
15
+
16
+ ## Features
17
+
18
+ - **WASM-first**: Compiled from Rust to WebAssembly for maximum performance and portability
19
+ - **Type-safe routing**: Full TypeScript support with auto-completed route definitions
20
+ - **Edge runtime support**: Works in browsers, Cloudflare Workers, Deno, and Node.js
21
+ - **Zero Node.js dependencies**: Pure fetch API—no Node globals required
22
+ - **Async/await native**: Seamless async/await for handlers and middleware
23
+ - **Lightweight**: Optimized WASM binaries with aggressive tree-shaking
24
+ - **Schema validation**: Built-in request/response validation with Zod
25
+ - **WebSocket & SSE**: Full support for real-time features on compatible runtimes
26
+ - **Testing utilities**: In-memory test client for easy unit testing
27
+ - **Code generation**: Generate TypeScript apps and tests from OpenAPI/AsyncAPI
18
28
 
19
29
  ## Installation
20
30
 
21
- **From npm:**
31
+ ### From npm
22
32
 
23
33
  ```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
34
+ npm install @spikard/wasm
35
+ # or with yarn
36
+ yarn add @spikard/wasm
37
+ # or with pnpm
38
+ pnpm add @spikard/wasm
31
39
  ```
32
40
 
33
- **From source:**
41
+ ### From source
34
42
 
35
43
  ```bash
36
44
  cd packages/wasm
37
45
  pnpm install
38
- pnpm build # emits ESM to dist/
46
+ pnpm build # outputs to dist/
39
47
  ```
40
48
 
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
49
  ## Quick Start
47
50
 
48
51
  ### Cloudflare Workers
49
52
 
50
53
  ```typescript
51
- import { Spikard, get, post, createFetchHandler } from "spikard-wasm";
52
- import { z } from "zod";
54
+ import { Spikard, createFetchHandler, get, post } from "@spikard/wasm";
53
55
 
54
56
  const app = new Spikard();
55
57
 
56
- get("/hello")(async () => ({
57
- message: "Hello from the edge!"
58
+ // Define routes with type safety
59
+ get("/hello", async (req) => ({
60
+ message: "Hello from the edge!",
61
+ timestamp: new Date().toISOString(),
58
62
  }));
59
63
 
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 };
64
+ post("/echo", async (req) => {
65
+ const body = await req.json();
66
+ return { echo: body };
70
67
  });
71
68
 
69
+ // Export as a Cloudflare Worker
72
70
  export default {
73
71
  fetch: createFetchHandler(app),
74
72
  };
@@ -77,663 +75,497 @@ export default {
77
75
  ### Deno
78
76
 
79
77
  ```typescript
80
- import { Spikard, get } from "npm:spikard-wasm";
78
+ import { Spikard, get, post } from "npm:@spikard/wasm@0.2.1";
81
79
 
82
80
  const app = new Spikard();
83
81
 
84
- get("/")(async () => ({
85
- message: "Hello from Deno!"
82
+ get("/hello", async (req) => ({
83
+ message: "Hello from Deno",
86
84
  }));
87
85
 
88
- Deno.serve({ port: 8000 }, (request) => {
89
- return app.handleRequest(request);
86
+ post("/api/users", async (req) => {
87
+ const data = await req.json();
88
+ return { created: true, id: Math.random() };
90
89
  });
90
+
91
+ Deno.serve({ port: 8000 }, (request) => app.handleRequest(request));
91
92
  ```
92
93
 
93
- ### Browser
94
+ ### Node.js / Bun
94
95
 
95
96
  ```typescript
96
- import { Spikard, get, TestClient } from "spikard-wasm";
97
+ import { Spikard, createFetchHandler, get } from "@spikard/wasm";
97
98
 
98
99
  const app = new Spikard();
99
100
 
100
- get("/api/data")(async () => ({
101
- timestamp: Date.now(),
102
- data: [1, 2, 3],
101
+ get("/api/status", async (req) => ({
102
+ status: "ok",
103
+ runtime: "node",
103
104
  }));
104
105
 
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());
106
+ const server = Bun.serve({
107
+ port: 3000,
108
+ fetch: createFetchHandler(app),
109
+ });
110
+
111
+ console.log(`Server running on http://localhost:${server.port}`);
109
112
  ```
110
113
 
111
- ## Route Registration
114
+ ### Browser (with bundler)
112
115
 
113
- ### Decorator-Style Registration
116
+ ```typescript
117
+ import { Spikard, get } from "@spikard/wasm";
118
+
119
+ const app = new Spikard();
114
120
 
115
- Routes are registered using HTTP method decorators:
121
+ get("/worker", async (req) => ({
122
+ message: "Running in a browser Web Worker",
123
+ }));
124
+
125
+ // Simulate incoming requests in a worker context
126
+ self.addEventListener("message", async (event) => {
127
+ const response = await app.handleRequest(event.data.request);
128
+ self.postMessage({ response });
129
+ });
130
+ ```
131
+
132
+ ## API Documentation
133
+
134
+ ### Routing Helpers
116
135
 
117
136
  ```typescript
118
- import { get, post, put, patch, del } from "spikard-wasm";
137
+ import { Spikard, get, post, put, patch, delete_, head, options } from "@spikard/wasm";
138
+
139
+ const app = new Spikard();
119
140
 
120
- get("/users")(async () => {
121
- return { users: [] };
141
+ // Define routes with automatic method binding
142
+ get("/users", async (req) => {
143
+ // GET /users
122
144
  });
123
145
 
124
- post("/users")(async (req) => {
125
- const user = req.json();
126
- return { created: true, user };
146
+ post("/users", async (req) => {
147
+ // POST /users with body parsing
148
+ const body = await req.json();
127
149
  });
128
150
 
129
- put("/users/:id")(async (req) => {
130
- const id = req.pathParams.id;
131
- return { id, updated: true };
151
+ put("/users/:id", async (req, { id }) => {
152
+ // PUT /users/:id with path params
132
153
  });
133
154
 
134
- patch("/users/:id")(async (req) => {
135
- return { id: req.pathParams.id, patched: true };
155
+ patch("/users/:id", async (req, { id }) => {
156
+ // PATCH /users/:id
136
157
  });
137
158
 
138
- del("/users/:id")(async (req) => {
139
- return { deleted: true };
159
+ delete_("/users/:id", async (req, { id }) => {
160
+ // DELETE /users/:id (note: delete_ to avoid keyword)
140
161
  });
141
162
  ```
142
163
 
143
- ### Manual Registration with `addRoute`
144
-
145
- For dynamic route registration:
164
+ ### Request Handling
146
165
 
147
166
  ```typescript
148
- import { Spikard } from "spikard-wasm";
167
+ // Access request properties
168
+ get("/example", async (req) => {
169
+ const method = req.method; // "GET"
170
+ const url = req.url; // Full URL
171
+ const headers = req.headers; // Headers object
149
172
 
150
- const app = new Spikard();
173
+ // Parse JSON body
174
+ const json = await req.json();
151
175
 
152
- async function listUsers() {
153
- return { users: [] };
154
- }
176
+ // Parse form data
177
+ const form = await req.formData();
155
178
 
156
- app.addRoute(
157
- {
158
- method: "GET",
159
- path: "/users",
160
- handler_name: "listUsers",
161
- is_async: true,
162
- },
163
- listUsers
164
- );
165
- ```
179
+ // Get raw text
180
+ const text = await req.text();
166
181
 
167
- ### Supported HTTP Methods
182
+ // Get ArrayBuffer
183
+ const buffer = await req.arrayBuffer();
168
184
 
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
185
+ return { received: true };
186
+ });
187
+ ```
177
188
 
178
- ### With Schemas
189
+ ### Response Building
190
+
191
+ ```typescript
192
+ import { Spikard, get, json, status, withHeaders } from "@spikard/wasm";
193
+
194
+ get("/users", async (req) => {
195
+ return json(
196
+ {
197
+ users: [
198
+ { id: 1, name: "Alice" },
199
+ { id: 2, name: "Bob" },
200
+ ],
201
+ },
202
+ {
203
+ status: 200,
204
+ headers: {
205
+ "X-Total-Count": "2",
206
+ "Cache-Control": "max-age=3600",
207
+ },
208
+ }
209
+ );
210
+ });
179
211
 
180
- Spikard WASM supports **Zod schemas** and **raw JSON Schema objects**.
212
+ get("/created", async (req) => {
213
+ return status(201, { id: 123, created: true });
214
+ });
215
+ ```
181
216
 
182
- **With Zod (recommended - type inference):**
217
+ ### Schema Validation with Zod
183
218
 
184
219
  ```typescript
185
- import { post } from "spikard-wasm";
186
220
  import { z } from "zod";
187
221
 
188
- const CreateUserSchema = z.object({
222
+ const userSchema = z.object({
189
223
  name: z.string().min(1),
190
224
  email: z.string().email(),
191
- age: z.number().int().min(18),
225
+ age: z.number().int().positive().optional(),
192
226
  });
193
227
 
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
- ```
228
+ post("/users", async (req) => {
229
+ const body = await req.json();
202
230
 
203
- **With raw JSON Schema:**
231
+ // Validate with Zod
232
+ const result = userSchema.safeParse(body);
204
233
 
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
- };
234
+ if (!result.success) {
235
+ return {
236
+ error: "Invalid user data",
237
+ issues: result.error.issues,
238
+ };
239
+ }
240
+
241
+ // result.data is now type-safe
242
+ const user = result.data;
214
243
 
215
- post("/users", { bodySchema: userSchema })(async function createUser(req) {
216
- const user = req.json<{ name: string; email: string }>();
217
- return { id: 1, ...user };
244
+ return json({ id: 1, ...user }, { status: 201 });
218
245
  });
219
246
  ```
220
247
 
221
- ## Request Handling
222
-
223
- ### Accessing Request Data
248
+ ### Testing with TestClient
224
249
 
225
250
  ```typescript
226
- get("/search")(async function search(req) {
227
- // Path parameters
228
- const userId = req.pathParams.id;
251
+ import { describe, it, expect } from "vitest";
252
+ import { Spikard, TestClient, get } from "@spikard/wasm";
229
253
 
230
- // Query parameters
231
- const params = new URLSearchParams(req.queryString);
232
- const q = params.get("q");
233
- const limit = params.get("limit") ?? "10";
254
+ describe("API routes", () => {
255
+ const app = new Spikard();
234
256
 
235
- // Headers
236
- const auth = req.headers["authorization"];
237
- const userAgent = req.headers["user-agent"];
257
+ get("/hello", async () => ({
258
+ message: "Hello",
259
+ }));
238
260
 
239
- // Cookies (if available)
240
- const sessionId = req.cookies?.session_id;
261
+ const client = new TestClient(app);
241
262
 
242
- // Method and path
243
- console.log(`${req.method} ${req.path}`);
263
+ it("returns greeting", async () => {
264
+ const res = await client.get("/hello");
244
265
 
245
- return { query: q, limit: parseInt(limit) };
246
- });
247
- ```
266
+ expect(res.status).toBe(200);
267
+ expect(res.json()).toEqual({
268
+ message: "Hello",
269
+ });
270
+ });
248
271
 
249
- ### JSON Body
272
+ it("handles POST with body", async () => {
273
+ post("/echo", async (req) => {
274
+ const body = await req.json();
275
+ return { echo: body };
276
+ });
250
277
 
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
278
+ const res = await client.post("/echo", { message: "test" });
259
279
 
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
- };
280
+ expect(res.status).toBe(200);
281
+ expect(res.json()).toEqual({
282
+ echo: { message: "test" },
283
+ });
284
+ });
267
285
  });
268
286
  ```
269
287
 
270
- ## Handler Wrappers
271
-
272
- For automatic parameter extraction:
288
+ ## Bundle Size
273
289
 
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
- }));
290
+ Optimized for minimal bundle size:
286
291
 
287
- // Full context wrapper
288
- get("/users/:id", {}, wrapHandler(async (params, query, body) => {
289
- return { id: params.id, query };
290
- }));
291
- ```
292
+ - **Uncompressed**: ~200KB (varies by feature set)
293
+ - **Gzip**: ~60KB
294
+ - **Brotli**: ~45KB
292
295
 
293
- ## File Uploads
296
+ Bundle size analysis:
294
297
 
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
- });
298
+ ```bash
299
+ # Use source-map-explorer or similar
300
+ npx source-map-explorer 'dist/**/*.js'
313
301
  ```
314
302
 
315
- ## Streaming Responses
316
-
317
- ```typescript
318
- import { StreamingResponse } from "spikard-wasm";
303
+ ## WebAssembly Configuration
319
304
 
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
- }
305
+ Compiled with aggressive optimizations in `Cargo.toml`:
326
306
 
327
- get("/stream")(async function stream() {
328
- return new StreamingResponse(generateData(), {
329
- statusCode: 200,
330
- headers: { "Content-Type": "application/x-ndjson" },
331
- });
332
- });
307
+ ```toml
308
+ [package.metadata.wasm-pack.profile.release]
309
+ wasm-opt = ["-O3", "--enable-bulk-memory", "--enable-nontrapping-float-to-int", "--enable-simd"]
333
310
  ```
334
311
 
335
- ### Server-Sent Events (SSE)
312
+ Build options:
336
313
 
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
- }
314
+ ```bash
315
+ # Development (debug symbols, fast compile)
316
+ wasm-pack build --dev
345
317
 
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
- });
318
+ # Release (optimized, minimal size)
319
+ wasm-pack build --release
355
320
  ```
356
321
 
357
- ## Configuration
358
-
359
- ```typescript
360
- import { Spikard, type ServerConfig } from "spikard-wasm";
322
+ ## Code Generation
361
323
 
362
- const app = new Spikard();
324
+ Generate TypeScript applications and tests from OpenAPI/AsyncAPI specifications:
363
325
 
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
- };
326
+ ```bash
327
+ # Generate from OpenAPI spec
328
+ spikard generate openapi \
329
+ --fixtures ../../testing_data \
330
+ --output ./generated
391
331
 
392
- // Apply configuration
393
- app.configure(config);
332
+ # Generate WebSocket handlers from AsyncAPI
333
+ spikard generate asyncapi \
334
+ --fixtures ../../testing_data/websockets \
335
+ --output ./generated
394
336
  ```
395
337
 
396
338
  ## Lifecycle Hooks
397
339
 
398
340
  ```typescript
399
- app.onRequest(async (request) => {
400
- console.log(`${request.method} ${request.path}`);
401
- return request;
402
- });
341
+ import { Spikard, HookTypes } from "@spikard/wasm";
403
342
 
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;
343
+ const app = new Spikard();
344
+
345
+ // On every request (before validation)
346
+ app.onRequest(async (req) => {
347
+ console.log(`${req.method} ${req.url}`);
413
348
  });
414
349
 
415
- app.preHandler(async (request) => {
416
- // After validation, before handler
417
- request.startTime = Date.now();
418
- return request;
350
+ // Before handler execution
351
+ app.preHandler(async (req) => {
352
+ // Add request ID, timing, etc.
419
353
  });
420
354
 
421
- app.onResponse(async (response) => {
422
- response.headers["X-Frame-Options"] = "DENY";
423
- response.headers["X-Content-Type-Options"] = "nosniff";
424
- return response;
355
+ // After response
356
+ app.onResponse(async (req, res) => {
357
+ console.log(`${req.method} ${req.url} -> ${res.status}`);
425
358
  });
426
359
 
427
- app.onError(async (response) => {
428
- console.error(`Error: ${response.status}`);
429
- return response;
360
+ // On error
361
+ app.onError(async (error, req) => {
362
+ console.error(`Error: ${error.message}`);
363
+ return { error: "Internal Server Error" };
430
364
  });
431
365
  ```
432
366
 
433
- ## Testing
367
+ ## Real-Time Features
434
368
 
435
- ### In-Memory Test Client
369
+ ### WebSocket Support
436
370
 
437
371
  ```typescript
438
- import { TestClient } from "spikard-wasm";
439
- import { expect } from "vitest";
372
+ import { Spikard, ws } from "@spikard/wasm";
440
373
 
441
374
  const app = new Spikard();
442
375
 
443
- get("/users/:id")(async (req) => {
444
- return { id: req.pathParams.id, name: "Alice" };
376
+ ws("/chat", {
377
+ onOpen: (socket) => {
378
+ console.log("Client connected");
379
+ },
380
+ onMessage: (socket, data) => {
381
+ socket.broadcast(data);
382
+ },
383
+ onClose: (socket) => {
384
+ console.log("Client disconnected");
385
+ },
445
386
  });
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
387
  ```
453
388
 
454
- ### WebSocket Testing
389
+ ### Server-Sent Events (SSE)
455
390
 
456
391
  ```typescript
457
- import { ws } from "spikard-wasm";
392
+ import { Spikard, sse } from "@spikard/wasm";
458
393
 
459
- ws("/ws")(async (socket) => {
460
- socket.on("message", (msg) => {
461
- socket.send({ echo: msg });
462
- });
463
- });
394
+ const app = new Spikard();
464
395
 
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
- ```
396
+ sse("/events", async (req, res) => {
397
+ res.write("data: " + JSON.stringify({ event: "connected" }) + "\n\n");
472
398
 
473
- ### SSE Testing
399
+ const interval = setInterval(() => {
400
+ res.write(
401
+ "data: " + JSON.stringify({ event: "ping", time: Date.now() }) + "\n\n"
402
+ );
403
+ }, 5000);
474
404
 
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);
405
+ return () => clearInterval(interval);
406
+ });
480
407
  ```
481
408
 
482
- ## Type Safety
483
-
484
- Full TypeScript support with auto-generated types:
409
+ ## Error Handling
485
410
 
486
411
  ```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
412
+ import { Spikard, HttpError } from "@spikard/wasm";
497
413
 
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
- ```
414
+ const app = new Spikard();
509
415
 
510
- ## Validation with Zod
416
+ get("/users/:id", async (req, { id }) => {
417
+ if (!id) {
418
+ throw new HttpError(400, "User ID is required");
419
+ }
511
420
 
512
- ```typescript
513
- import { z } from "zod";
421
+ const user = await fetchUser(id);
514
422
 
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
- });
423
+ if (!user) {
424
+ throw new HttpError(404, `User ${id} not found`);
425
+ }
521
426
 
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
427
  return user;
526
428
  });
429
+
430
+ // Automatic error response
431
+ // 404 -> { error: "User 123 not found", status: 404 }
527
432
  ```
528
433
 
529
- ## Performance
434
+ ## CI Benchmarks (2025-12-20)
435
+
436
+ Run: `snapshots/benchmarks/20397054933` (commit `25e4fdf`, oha, 50 concurrency, 10s, Linux x86_64).
530
437
 
531
- ### Benchmark Results
438
+ | Metric | Value |
439
+ | --- | --- |
440
+ | Avg RPS (all workloads) | 10,658 |
441
+ | Avg latency (ms) | 5.70 |
532
442
 
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`.
443
+ Category breakdown:
534
444
 
535
- | Binding | Avg RPS (all workloads) | Avg latency (ms) |
445
+ | Category | Avg RPS | Avg latency (ms) |
536
446
  | --- | --- | --- |
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 |
447
+ | path-params | 15,841 | 3.19 |
448
+ | multipart | 10,838 | 5.24 |
449
+ | query-params | 8,082 | 6.88 |
450
+ | json-bodies | 6,890 | 7.37 |
451
+ | forms | 6,241 | 8.80 |
543
452
 
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
453
+ ## Performance Tips
547
454
 
548
- ### Runtime Characteristics
455
+ 1. **Lazy load routes**: Only define routes you need to minimize WASM size
456
+ 2. **Compression**: Enable Brotli/Gzip compression for responses
457
+ 3. **Caching**: Use Cache-Control headers for static content
458
+ 4. **Streaming**: For large responses, use streaming responses
459
+ 5. **Worker threads**: Offload heavy computation to Web Workers
549
460
 
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
461
+ ## Environment Variables
557
462
 
558
- ### Bundle Size Optimization
463
+ Access environment variables based on runtime:
559
464
 
560
465
  ```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();
466
+ // Cloudflare Workers
467
+ get("/env", async (req, { env }) => {
468
+ const apiKey = env.API_KEY;
469
+ return { apiKey };
470
+ });
574
471
 
575
- get("/")(async (req) => {
576
- return {
577
- message: "Hello from Cloudflare Workers",
578
- cf: req.cf, // Cloudflare-specific properties
579
- };
472
+ // Deno
473
+ get("/env", async (req) => {
474
+ const apiKey = Deno.env.get("API_KEY");
475
+ return { apiKey };
580
476
  });
581
477
 
582
- export default {
583
- fetch: createFetchHandler(app),
584
- };
478
+ // Node.js / Bun
479
+ get("/env", async (req) => {
480
+ const apiKey = process.env.API_KEY;
481
+ return { apiKey };
482
+ });
585
483
  ```
586
484
 
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
- ```
485
+ ## Debugging
601
486
 
602
- ### Vercel Edge Functions
487
+ Enable debug logging:
603
488
 
604
489
  ```typescript
605
- import { Spikard, get, createFetchHandler } from "spikard-wasm";
606
-
607
- const app = new Spikard();
490
+ const app = new Spikard({ debug: true });
608
491
 
609
- get("/api/hello")(async () => ({ message: "Hello from Vercel Edge" }));
610
-
611
- export const config = { runtime: "edge" };
612
- export default createFetchHandler(app);
492
+ // Or via environment
493
+ // SPIKARD_DEBUG=1 npm run dev
613
494
  ```
614
495
 
615
- ### Browser (Service Worker)
496
+ ## Examples
616
497
 
617
- ```typescript
618
- import { Spikard, get } from "spikard-wasm";
498
+ Full working examples for different runtimes:
619
499
 
620
- const app = new Spikard();
500
+ - **[Rollup Bundler](https://github.com/Goldziher/spikard/tree/main/examples/wasm-rollup)** - Build with Rollup for browsers and Node.js
501
+ - **[Deno Runtime](https://github.com/Goldziher/spikard/tree/main/examples/wasm-deno)** - Native Deno with zero build step
502
+ - **[Cloudflare Workers](https://github.com/Goldziher/spikard/tree/main/examples/wasm-cloudflare)** - Deploy to the edge with Wrangler
621
503
 
622
- get("/api/data")(async () => ({
623
- cached: true,
624
- timestamp: Date.now(),
625
- }));
504
+ Each example includes:
505
+ - Complete TypeScript source code with strict typing
506
+ - Configuration files (tsconfig.json, package.json)
507
+ - Comprehensive README with usage instructions
508
+ - Consistent API routes demonstrating core features
626
509
 
627
- self.addEventListener("fetch", (event) => {
628
- if (event.request.url.includes("/api/")) {
629
- event.respondWith(app.handleRequest(event.request));
630
- }
631
- });
632
- ```
510
+ Browse all examples: [`examples/`](https://github.com/Goldziher/spikard/tree/main/examples)
633
511
 
634
- ## Code Generation
512
+ ## Testing
635
513
 
636
- Generate type-safe WASM applications from OpenAPI/AsyncAPI specs:
514
+ Run tests with Vitest:
637
515
 
638
516
  ```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
517
+ pnpm test # Run all tests
518
+ pnpm test:watch # Watch mode
519
+ pnpm test:coverage # With coverage
650
520
  ```
651
521
 
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
522
+ Test coverage minimum: **80%**
661
523
 
662
- ## Development Notes
663
-
664
- ### Building from Source
524
+ Run integration tests:
665
525
 
666
526
  ```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
527
+ task test:wasm
528
+ task test:wasm:integration
677
529
  ```
678
530
 
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
- ```
531
+ ## Documentation
691
532
 
692
- ### Debugging WASM
533
+ - **API Docs**: [docs/api.md](./docs/api.md)
534
+ - **Architecture**: [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)
535
+ - **Architecture Decision Records**: [../../docs/adr/](../../docs/adr/)
536
+ - [ADR 0001: Architecture & Layering](../../docs/adr/0001-architecture.md)
537
+ - [ADR 0002: Tower-HTTP & Middleware](../../docs/adr/0002-runtime-and-middleware.md)
538
+ - [ADR 0006: Async & Streaming](../../docs/adr/0006-async-and-streaming.md)
693
539
 
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
540
+ ## TypeScript Support
699
541
 
700
- ## Differences from Node.js Bindings
542
+ Full TypeScript support with strict type checking:
701
543
 
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
544
+ ```bash
545
+ pnpm typecheck # Run tsc
546
+ pnpm lint # Run Biome
547
+ ```
708
548
 
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
549
+ Generated `.d.ts` files via wasm-bindgen for complete IDE support.
715
550
 
716
- ### When to Use WASM vs Node.js
551
+ ## Contributing
717
552
 
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
553
+ Contributions welcome! See [CONTRIBUTING.md](../../CONTRIBUTING.md)
723
554
 
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
555
+ Code standards:
556
+ - TypeScript 5.x with strict mode enabled
557
+ - Biome for linting and formatting
558
+ - Vitest for testing
559
+ - 80%+ test coverage required
729
560
 
730
- ## Documentation
561
+ ## Related Packages
731
562
 
732
- - [Main Project README](../../README.md)
733
- - [Contributing Guide](../../CONTRIBUTING.md)
734
- - [TypeScript API Reference](./src/index.ts)
735
- - [Architecture Decision Records](../../docs/adr/)
563
+ - **@spikard/node**: [npm.im/@spikard/node](https://npm.im/@spikard/node) - Node.js native bindings
564
+ - **spikard**: [pypi.org/project/spikard](https://pypi.org/project/spikard) - Python bindings
565
+ - **spikard**: [rubygems.org/gems/spikard](https://rubygems.org/gems/spikard) - Ruby bindings
566
+ - **spikard/spikard**: [packagist.org/packages/spikard/spikard](https://packagist.org/packages/spikard/spikard) - PHP bindings
567
+ - **spikard**: [crates.io/crates/spikard](https://crates.io/crates/spikard) - Rust native
736
568
 
737
569
  ## License
738
570
 
739
- MIT
571
+ MIT - see [LICENSE](LICENSE) file