@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.
- package/README.md +356 -524
- package/dist/chunk-YO5KBN4A.mjs +2268 -0
- package/dist/chunk-YO5KBN4A.mjs.map +1 -0
- package/dist/index.d.mts +365 -0
- package/dist/index.d.ts +365 -0
- package/dist/index.js +2270 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node.d.mts +9 -0
- package/dist/node.d.ts +9 -0
- package/dist/node.js +2401 -0
- package/dist/node.js.map +1 -0
- package/dist/node.mjs +120 -0
- package/dist/node.mjs.map +1 -0
- package/{dist-node → dist}/package.json +1 -1
- package/{dist-node → dist}/spikard_wasm.js +11 -8
- package/{dist-node → dist}/spikard_wasm_bg.wasm +0 -0
- package/package.json +54 -46
- package/dist-bundler/package.json +0 -34
- package/dist-bundler/spikard_wasm.d.ts +0 -28
- package/dist-bundler/spikard_wasm.js +0 -5
- package/dist-bundler/spikard_wasm_bg.js +0 -914
- package/dist-bundler/spikard_wasm_bg.wasm +0 -0
- package/dist-bundler/spikard_wasm_bg.wasm.d.ts +0 -26
- package/dist-node/README.md +0 -739
- package/dist-node/spikard_wasm.d.ts +0 -28
- package/dist-node/spikard_wasm_bg.wasm.d.ts +0 -26
- package/dist-web/README.md +0 -739
- package/dist-web/package.json +0 -32
- package/dist-web/spikard_wasm.d.ts +0 -79
- package/dist-web/spikard_wasm.js +0 -921
- package/dist-web/spikard_wasm_bg.wasm +0 -0
- package/dist-web/spikard_wasm_bg.wasm.d.ts +0 -26
- /package/{dist-bundler → dist}/README.md +0 -0
package/dist-node/README.md
DELETED
|
@@ -1,739 +0,0 @@
|
|
|
1
|
-
# spikard-wasm
|
|
2
|
-
|
|
3
|
-
WebAssembly bindings for Spikard HTTP framework via wasm-bindgen.
|
|
4
|
-
|
|
5
|
-
## Status & Badges
|
|
6
|
-
|
|
7
|
-
[](https://spikard.dev)
|
|
8
|
-
[](https://www.npmjs.com/package/spikard-wasm)
|
|
9
|
-
[](https://www.npmjs.com/package/spikard-wasm)
|
|
10
|
-
[](https://crates.io/crates/spikard-wasm)
|
|
11
|
-
[](https://docs.rs/spikard-wasm)
|
|
12
|
-
[](LICENSE)
|
|
13
|
-
[](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
|