@spikard/node 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Na'aman Hirschfeld
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,509 @@
1
+ # spikard-node
2
+
3
+ High-performance Node.js bindings for Spikard HTTP framework via napi-rs.
4
+
5
+ ## Status & Badges
6
+
7
+ [![npm](https://img.shields.io/npm/v/spikard.svg)](https://www.npmjs.com/package/spikard)
8
+ [![npm downloads](https://img.shields.io/npm/dm/spikard.svg)](https://www.npmjs.com/package/spikard)
9
+ [![Crates.io](https://img.shields.io/crates/v/spikard-node.svg)](https://crates.io/crates/spikard-node)
10
+ [![Documentation](https://docs.rs/spikard-node/badge.svg)](https://docs.rs/spikard-node)
11
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
+ [![Discord](https://img.shields.io/badge/Discord-Join%20our%20community-7289da)](https://discord.gg/pXxagNK2zN)
13
+
14
+ ## Overview
15
+
16
+ High-performance TypeScript/Node.js web framework with a Rust core. Build REST APIs with type-safe routing backed by Axum and Tower-HTTP.
17
+
18
+ ## Installation
19
+
20
+ **From source (currently):**
21
+
22
+ ```bash
23
+ cd packages/node
24
+ pnpm install
25
+ pnpm build
26
+ ```
27
+
28
+ **Requirements:**
29
+ - Node.js 20+
30
+ - pnpm 10+
31
+ - Rust toolchain (for building from source)
32
+
33
+ ## Quick Start
34
+
35
+ ```typescript
36
+ import { Spikard, type Request } from "spikard";
37
+ import { z } from "zod";
38
+
39
+ const UserSchema = z.object({
40
+ id: z.number(),
41
+ name: z.string(),
42
+ email: z.string().email(),
43
+ });
44
+
45
+ type User = z.infer<typeof UserSchema>;
46
+
47
+ const app = new Spikard();
48
+
49
+ const getUser = async (req: Request): Promise<User> => {
50
+ const id = Number(req.params["id"] ?? 0);
51
+ return { id, name: "Alice", email: "alice@example.com" };
52
+ };
53
+
54
+ const createUser = async (req: Request): Promise<User> => {
55
+ return UserSchema.parse(req.json());
56
+ };
57
+
58
+ app.addRoute(
59
+ {
60
+ method: "GET",
61
+ path: "/users/:id",
62
+ handler_name: "getUser",
63
+ is_async: true,
64
+ },
65
+ getUser,
66
+ );
67
+
68
+ app.addRoute(
69
+ {
70
+ method: "POST",
71
+ path: "/users",
72
+ handler_name: "createUser",
73
+ request_schema: UserSchema,
74
+ response_schema: UserSchema,
75
+ is_async: true,
76
+ },
77
+ createUser,
78
+ );
79
+
80
+ if (require.main === module) {
81
+ app.run({ port: 8000 });
82
+ }
83
+ ```
84
+
85
+ ## Route Registration
86
+
87
+ ### Manual Registration with `addRoute`
88
+
89
+ Routes are registered manually using `app.addRoute(metadata, handler)`:
90
+
91
+ ```typescript
92
+ import { Spikard, type Request } from "spikard";
93
+
94
+ const app = new Spikard();
95
+
96
+ async function listUsers(_req: Request): Promise<{ users: unknown[] }> {
97
+ return { users: [] };
98
+ }
99
+
100
+ async function createUser(_req: Request): Promise<{ created: boolean }> {
101
+ return { created: true };
102
+ }
103
+
104
+ app.addRoute(
105
+ {
106
+ method: "GET",
107
+ path: "/users",
108
+ handler_name: "listUsers",
109
+ is_async: true,
110
+ },
111
+ listUsers
112
+ );
113
+
114
+ app.addRoute(
115
+ {
116
+ method: "POST",
117
+ path: "/users",
118
+ handler_name: "createUser",
119
+ is_async: true,
120
+ },
121
+ createUser
122
+ );
123
+ ```
124
+
125
+ ### Supported HTTP Methods
126
+
127
+ - `GET` - Retrieve resources
128
+ - `POST` - Create resources
129
+ - `PUT` - Replace resources
130
+ - `PATCH` - Update resources
131
+ - `DELETE` - Delete resources
132
+ - `HEAD` - Get headers only
133
+ - `OPTIONS` - Get allowed methods
134
+ - `TRACE` - Echo the request
135
+
136
+ ### With Schemas
137
+
138
+ Spikard supports **Zod schemas** and **raw JSON Schema objects**.
139
+
140
+ **With Zod (recommended - type inference):**
141
+
142
+ ```typescript
143
+ import { post } from "spikard";
144
+ import { z } from "zod";
145
+
146
+ const CreateUserSchema = z.object({
147
+ name: z.string().min(1),
148
+ email: z.string().email(),
149
+ age: z.number().int().min(18),
150
+ });
151
+
152
+ post("/users", {
153
+ bodySchema: CreateUserSchema,
154
+ responseSchema: z.object({ id: z.number(), name: z.string() }),
155
+ })(async function createUser(req) {
156
+ const user = req.json();
157
+ return { id: 1, name: user.name };
158
+ });
159
+ ```
160
+
161
+ **With raw JSON Schema:**
162
+
163
+ ```typescript
164
+ const userSchema = {
165
+ type: "object",
166
+ properties: {
167
+ name: { type: "string" },
168
+ email: { type: "string", format: "email" },
169
+ },
170
+ required: ["name", "email"],
171
+ };
172
+
173
+ post("/users", { bodySchema: userSchema })(async function createUser(req) {
174
+ const user = req.json<{ name: string; email: string }>();
175
+ return { id: 1, ...user };
176
+ });
177
+ ```
178
+
179
+ ## Request Handling
180
+
181
+ ### Accessing Request Data
182
+
183
+ ```typescript
184
+ get("/search")(async function search(req) {
185
+ // Query parameters
186
+ const params = new URLSearchParams(req.queryString);
187
+ const q = params.get("q");
188
+ const limit = params.get("limit") ?? "10";
189
+
190
+ // Headers
191
+ const auth = req.headers["authorization"];
192
+
193
+ // Method and path
194
+ console.log(`${req.method} ${req.path}`);
195
+
196
+ return { query: q, limit: parseInt(limit) };
197
+ });
198
+ ```
199
+
200
+ ### JSON Body
201
+
202
+ ```typescript
203
+ post("/users")(async function createUser(req) {
204
+ const body = req.json<{ name: string; email: string }>();
205
+ return { id: 1, ...body };
206
+ });
207
+ ```
208
+
209
+ ### Form Data
210
+
211
+ ```typescript
212
+ post("/login")(async function login(req) {
213
+ const form = req.form();
214
+ return {
215
+ username: form.username,
216
+ password: form.password,
217
+ };
218
+ });
219
+ ```
220
+
221
+ ## Handler Wrappers
222
+
223
+ For automatic parameter extraction:
224
+
225
+ ```typescript
226
+ import { wrapHandler, wrapBodyHandler } from "spikard";
227
+
228
+ // Body-only wrapper
229
+ post("/users", {}, wrapBodyHandler(async (body: CreateUserRequest) => {
230
+ return { id: 1, name: body.name };
231
+ }));
232
+
233
+ // Full context wrapper
234
+ get(
235
+ "/users/:id",
236
+ {},
237
+ wrapHandler(async (params: { id: string }, query: Record<string, unknown>) => {
238
+ return { id: Number(params.id), query };
239
+ }),
240
+ );
241
+ ```
242
+
243
+ ## File Uploads
244
+
245
+ ```typescript
246
+ import { UploadFile } from "spikard";
247
+
248
+ interface UploadRequest {
249
+ file: UploadFile;
250
+ description: string;
251
+ }
252
+
253
+ post("/upload")(async function upload(req) {
254
+ const body = req.json<UploadRequest>();
255
+ const content = body.file.read();
256
+
257
+ return {
258
+ filename: body.file.filename,
259
+ size: body.file.size,
260
+ contentType: body.file.contentType,
261
+ };
262
+ });
263
+ ```
264
+
265
+ ## Streaming Responses
266
+
267
+ ```typescript
268
+ import { StreamingResponse } from "spikard";
269
+
270
+ async function* generateData() {
271
+ for (let i = 0; i < 10; i++) {
272
+ yield JSON.stringify({ count: i }) + "\n";
273
+ await new Promise((resolve) => setTimeout(resolve, 100));
274
+ }
275
+ }
276
+
277
+ get("/stream")(async function stream() {
278
+ return new StreamingResponse(generateData(), {
279
+ statusCode: 200,
280
+ headers: { "Content-Type": "application/x-ndjson" },
281
+ });
282
+ });
283
+ ```
284
+
285
+ ## Configuration
286
+
287
+ ```typescript
288
+ import { Spikard, runServer, type ServerConfig } from "spikard";
289
+
290
+ const app = new Spikard();
291
+
292
+ const config: ServerConfig = {
293
+ host: "0.0.0.0",
294
+ port: 8080,
295
+ workers: 4,
296
+ enableRequestId: true,
297
+ maxBodySize: 10 * 1024 * 1024, // 10 MB
298
+ requestTimeout: 30, // seconds
299
+ compression: {
300
+ gzip: true,
301
+ brotli: true,
302
+ quality: 9,
303
+ minSize: 1024,
304
+ },
305
+ rateLimit: {
306
+ perSecond: 100,
307
+ burst: 200,
308
+ ipBased: true,
309
+ },
310
+ jwtAuth: {
311
+ secret: "your-secret-key",
312
+ algorithm: "HS256",
313
+ },
314
+ staticFiles: [
315
+ {
316
+ directory: "./public",
317
+ routePrefix: "/static",
318
+ indexFile: true,
319
+ },
320
+ ],
321
+ openapi: {
322
+ enabled: true,
323
+ title: "My API",
324
+ version: "1.0.0",
325
+ swaggerUiPath: "/docs",
326
+ redocPath: "/redoc",
327
+ },
328
+ };
329
+
330
+ runServer(app, config);
331
+ ```
332
+
333
+ ## Lifecycle Hooks
334
+
335
+ ```typescript
336
+ app.onRequest(async (request) => {
337
+ console.log(`${request.method} ${request.path}`);
338
+ return request;
339
+ });
340
+
341
+ app.preValidation(async (request) => {
342
+ // Check before validation
343
+ if (!request.headers["authorization"]) {
344
+ return {
345
+ status: 401,
346
+ body: { error: "Unauthorized" },
347
+ };
348
+ }
349
+ return request;
350
+ });
351
+
352
+ app.preHandler(async (request) => {
353
+ // After validation, before handler
354
+ return request;
355
+ });
356
+
357
+ app.onResponse(async (response) => {
358
+ response.headers["X-Frame-Options"] = "DENY";
359
+ return response;
360
+ });
361
+
362
+ app.onError(async (response) => {
363
+ console.error(`Error: ${response.status}`);
364
+ return response;
365
+ });
366
+ ```
367
+
368
+ ## Background Tasks
369
+
370
+ ```typescript
371
+ import * as background from "spikard/background";
372
+
373
+ post("/process")(async function process(req) {
374
+ const data = req.json();
375
+
376
+ background.run(() => {
377
+ // Heavy processing after response sent
378
+ processData(data);
379
+ });
380
+
381
+ return { status: "processing" };
382
+ });
383
+ ```
384
+
385
+ ## Testing
386
+
387
+ ```typescript
388
+ import { TestClient } from "spikard";
389
+ import { expect } from "vitest";
390
+
391
+ const app = {
392
+ routes: [
393
+ /* ... */
394
+ ],
395
+ handlers: {
396
+ /* ... */
397
+ },
398
+ };
399
+
400
+ const client = new TestClient(app);
401
+
402
+ const response = await client.get("/users/123");
403
+ expect(response.statusCode).toBe(200);
404
+ expect(response.json()).toEqual({ id: "123", name: "Alice" });
405
+ ```
406
+
407
+ ### WebSocket Testing
408
+
409
+ ```typescript
410
+ const ws = await client.websocketConnect("/ws");
411
+ await ws.sendJson({ message: "hello" });
412
+ const response = await ws.receiveJson();
413
+ expect(response.echo.message).toBe("hello");
414
+ await ws.close();
415
+ ```
416
+
417
+ ### SSE Testing
418
+
419
+ ```typescript
420
+ const response = await client.get("/events");
421
+ const sse = new SseStream(response.text());
422
+ const events = sse.eventsAsJson();
423
+ expect(events.length).toBeGreaterThan(0);
424
+ ```
425
+
426
+ ## Type Safety
427
+
428
+ Full TypeScript support with auto-generated types:
429
+
430
+ ```typescript
431
+ import {
432
+ type Request,
433
+ type Response,
434
+ type ServerConfig,
435
+ type RouteOptions,
436
+ type HandlerFunction,
437
+ } from "spikard";
438
+ ```
439
+
440
+ ### Parameter Types
441
+
442
+ ```typescript
443
+ import { Query, Path, Body, QueryDefault } from "spikard";
444
+
445
+ function handler(
446
+ id: Path<number>,
447
+ limit: Query<string | undefined>,
448
+ body: Body<UserType>
449
+ ) {
450
+ // Full type inference
451
+ }
452
+ ```
453
+
454
+ ## Validation with Zod
455
+
456
+ ```typescript
457
+ import { z } from "zod";
458
+
459
+ const UserSchema = z.object({
460
+ name: z.string().min(1).max(100),
461
+ email: z.string().email(),
462
+ age: z.number().int().min(18).optional(),
463
+ tags: z.array(z.string()).default([]),
464
+ });
465
+
466
+ post("/users", { bodySchema: UserSchema })(async function createUser(req) {
467
+ const user = req.json<z.infer<typeof UserSchema>>();
468
+ // user is fully typed and validated
469
+ return user;
470
+ });
471
+ ```
472
+
473
+ ## Running the Server
474
+
475
+ ```typescript
476
+ // Simple start
477
+ app.run({ port: 8000 });
478
+
479
+ // With full configuration
480
+ import { runServer } from "spikard";
481
+
482
+ runServer(app, {
483
+ host: "0.0.0.0",
484
+ port: 8080,
485
+ workers: 4,
486
+ });
487
+ ```
488
+
489
+ ## Performance
490
+
491
+ Node.js bindings use:
492
+ - **napi-rs** for zero-copy FFI
493
+ - **ThreadsafeFunction** for async JavaScript callbacks
494
+ - Dedicated Tokio runtime (doesn't block Node event loop)
495
+ - Direct type conversion without JSON serialization overhead
496
+
497
+ ## Examples
498
+
499
+ See `/examples/node/` for more examples.
500
+
501
+ ## Documentation
502
+
503
+ - [Main Project README](../../README.md)
504
+ - [Contributing Guide](../../CONTRIBUTING.md)
505
+ - [TypeScript API Reference](./src/index.ts)
506
+
507
+ ## License
508
+
509
+ MIT